viveworker 0.7.0-beta.3 → 0.8.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.
Files changed (34) hide show
  1. package/README.md +33 -4
  2. package/package.json +7 -3
  3. package/scripts/lib/remote-pairing/README.md +164 -0
  4. package/scripts/lib/remote-pairing/_browser-bundle-entry.mjs +86 -0
  5. package/scripts/lib/remote-pairing/audit.mjs +122 -0
  6. package/scripts/lib/remote-pairing/bridge-relay-client.mjs +737 -0
  7. package/scripts/lib/remote-pairing/control.mjs +156 -0
  8. package/scripts/lib/remote-pairing/envelope.mjs +224 -0
  9. package/scripts/lib/remote-pairing/http-dispatch.mjs +530 -0
  10. package/scripts/lib/remote-pairing/keys-core.mjs +110 -0
  11. package/scripts/lib/remote-pairing/keys.mjs +181 -0
  12. package/scripts/lib/remote-pairing/noise.mjs +436 -0
  13. package/scripts/lib/remote-pairing/orchestrator.mjs +356 -0
  14. package/scripts/lib/remote-pairing/pairings.mjs +446 -0
  15. package/scripts/lib/remote-pairing/rpc.mjs +381 -0
  16. package/scripts/moltbook-scout-auto.sh +16 -8
  17. package/scripts/share-cli.mjs +14 -130
  18. package/scripts/viveworker-bridge.mjs +1696 -223
  19. package/scripts/viveworker.mjs +27 -6
  20. package/web/app.css +727 -9
  21. package/web/app.js +1810 -232
  22. package/web/i18n.js +207 -17
  23. package/web/index.html +115 -1
  24. package/web/remote-pairing/api-router.js +873 -0
  25. package/web/remote-pairing/keys.js +237 -0
  26. package/web/remote-pairing/pairing-state.js +313 -0
  27. package/web/remote-pairing/rpc-client.js +765 -0
  28. package/web/remote-pairing/transport.js +804 -0
  29. package/web/remote-pairing/wake.js +149 -0
  30. package/web/remote-pairing-test.html +400 -0
  31. package/web/remote-pairing.bundle.js +3 -0
  32. package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
  33. package/web/remote-pairing.bundle.js.map +7 -0
  34. package/web/sw.js +190 -20
package/web/i18n.js CHANGED
@@ -25,7 +25,14 @@ const translations = {
25
25
  "common.completed": "Completed",
26
26
  "common.clear": "Clear",
27
27
  "common.cancel": "Cancel",
28
+ "common.close": "Close",
28
29
  "common.loading": "Loading",
30
+ "boot.status.starting": "Starting",
31
+ "boot.status.checkingLan": "Checking your trusted Wi-Fi...",
32
+ "boot.status.switchingRemote": "Switching to remote connection...",
33
+ "boot.status.establishingRemote": "Establishing encrypted session...",
34
+ "boot.status.loadingData": "Loading your workspace...",
35
+ "boot.status.slowHint": "The first remote connection can take tens of seconds.",
29
36
  "common.unavailable": "unavailable",
30
37
  "common.yes": "Yes",
31
38
  "common.no": "No",
@@ -112,6 +119,12 @@ const translations = {
112
119
  "Notifications are currently blocked for this app. You can re-enable them later from your device settings.",
113
120
  "banner.push.copy.default":
114
121
  "Enable push notifications to catch approvals, plans, questions, and completions even when the app is in the background.",
122
+ "banner.clientUpdate.title": "Update viveworker on this device",
123
+ "banner.clientUpdate.copy":
124
+ "Your Mac is running a newer viveworker app. Open this device on the same Wi-Fi or USB connection, then update so 4G relay features use the latest code.",
125
+ "banner.clientUpdate.action": "Update and reload",
126
+ "error.clientUpdateNeedsLan":
127
+ "Connect this device to the same Wi-Fi or USB path as your Mac, then try updating again.",
115
128
  "install.guide.title": "Add to Home Screen",
116
129
  "install.guide.intro.safari":
117
130
  "This browser does not let websites trigger this directly, but you can add viveworker in a few taps.",
@@ -156,6 +169,8 @@ const translations = {
156
169
  "detail.autoPilot.writeSourceContinuity": "Small code patches needs a recent read of the same file in this thread before Auto Pilot will approve it.",
157
170
  "detail.autoPilot.writeNoLaneMatch": "This change did not fit any enabled low-risk write lane in the current workspace.",
158
171
  "detail.imageAlt": ({ index }) => `Attached image ${index}`,
172
+ "detail.imageUnavailable": "Image unavailable",
173
+ "detail.imageUnavailableHint": "Mac file access is needed",
159
174
  "detail.filesTitle": "Files",
160
175
  "detail.diffTitle": "Diff",
161
176
  "detail.diffUnavailable": "Diff is not available for this change. Showing touched files only.",
@@ -182,6 +197,8 @@ const translations = {
182
197
  "reply.eyebrow": "Reply",
183
198
  "reply.title": "Send a follow-up",
184
199
  "reply.copy": "Send a new message back into this {provider} thread from the latest completed result.",
200
+ "reply.dockHint": "Tap to write a follow-up without moving the result.",
201
+ "reply.draftHint": "Draft in progress",
185
202
  "reply.fieldLabel": "Message",
186
203
  "reply.placeholder": "Ask {provider} to continue, refine the result, or try a different approach.",
187
204
  "reply.imageLabel": "Image",
@@ -250,6 +267,7 @@ const translations = {
250
267
  "timeline.filterLabel": "Thread filter",
251
268
  "timeline.status.approved": "Approved",
252
269
  "timeline.status.rejected": "Rejected",
270
+ "timeline.status.failed": "Failed",
253
271
  "timeline.status.implemented": "Started",
254
272
  "timeline.status.dismissed": "Dismissed",
255
273
  "timeline.status.submitted": "Sent",
@@ -312,6 +330,7 @@ const translations = {
312
330
  "settings.nextStep.test.title": "Everything is ready",
313
331
  "settings.nextStep.test.copy": "Send a test notification to confirm this device can receive pushes from viveworker.",
314
332
  "settings.status.notAvailable": "Not available",
333
+ "settings.status.supported": "Supported",
315
334
  "settings.status.unsupported": "Not supported",
316
335
  "settings.status.needsHttps": "Needs HTTPS",
317
336
  "settings.status.needsHomeScreen": "Needs Home Screen",
@@ -321,6 +340,7 @@ const translations = {
321
340
  "settings.status.installed": "Installed",
322
341
  "settings.status.notInstalled": "Not installed",
323
342
  "settings.status.connected": "Connected",
343
+ "settings.status.disconnected": "Disconnected",
324
344
  "settings.status.enabled": "Enabled",
325
345
  "settings.status.disabled": "Disabled",
326
346
  "settings.status.info": "Info",
@@ -372,7 +392,6 @@ const translations = {
372
392
  "settings.action.sendTest": "Send Test Notification",
373
393
  "settings.action.reviewTechnical": "Review technical details",
374
394
  "settings.action.revokeDevice": "Revoke",
375
- "settings.action.removeThisDevice": "Remove this device",
376
395
  "settings.install.title": "Install to Home Screen",
377
396
  "settings.install.copy":
378
397
  "For the best mobile experience, add viveworker to your Home Screen and open the installed app.",
@@ -382,7 +401,7 @@ const translations = {
382
401
  "settings.device.section.current": "This device",
383
402
  "settings.device.section.other": "Other trusted devices",
384
403
  "settings.device.thisDevice": "This device",
385
- "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.",
404
+ "settings.device.emptyCurrent": "This browser is not matched to an active LAN trusted-device record right now. If you're connected remotely, viveworker may still be using your remote pairing; otherwise re-pair from your Mac with `npx viveworker pair`.",
386
405
  "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.",
387
406
  "settings.device.addAnother.title": "Add another device",
388
407
  "settings.device.addAnother.heading": "Add a new device or browser",
@@ -477,6 +496,75 @@ const translations = {
477
496
  "settings.row.a2aTaskCompleted": "Tasks completed",
478
497
  "settings.row.a2aTaskDenied": "Tasks denied",
479
498
  "settings.a2aRelay.taskStats.title": "Task history",
499
+ "settings.remotePairing.title": "Remote connection",
500
+ "settings.remotePairing.subtitle": "Use paired devices off Wi-Fi",
501
+ "settings.remotePairing.copy": "Reach the PC from paired devices when you are away from the local network.",
502
+ "settings.remotePairing.connection.title": "Remote connection",
503
+ "settings.remotePairing.connection.copy": "Let the PC wait for encrypted remote connections through the relay.",
504
+ "settings.remotePairing.security.copy": "Only paired devices can connect. Traffic is end-to-end encrypted.",
505
+ "settings.remotePairing.toggle.title": "Remote access",
506
+ "settings.remotePairing.toggle.description": "When on, the PC waits for encrypted remote connections through the relay. Turn off to return to LAN-only access.",
507
+ "settings.remotePairing.toggle.offConfirm": "Turn off Remote connection? Devices outside Wi-Fi will no longer be able to reach this PC. To turn it back on, open viveworker on the same Wi-Fi as the PC.",
508
+ "settings.remotePairing.status.title": "Current status",
509
+ "settings.remotePairing.status.disabled": "Off",
510
+ "settings.remotePairing.status.waiting": "Waiting",
511
+ "settings.remotePairing.status.available": "Reachable",
512
+ "settings.remotePairing.status.connected": "Connected",
513
+ "settings.remotePairing.status.connecting": "Waiting…",
514
+ "settings.remotePairing.relayUrl.title": "Relay URL",
515
+ "settings.remotePairing.relayUrl.placeholder": "wss://pairing.viveworker.com",
516
+ "settings.remotePairing.relayUrl.help": "Leave blank to use the default. Use wss:// unless you are testing ws://localhost.",
517
+ "settings.remotePairing.relayUrl.save": "Save",
518
+ "settings.remotePairing.identity.title": "Bridge identity",
519
+ "settings.remotePairing.identity.fingerprint": "Fingerprint",
520
+ "settings.remotePairing.identity.empty": "(generated on first enable)",
521
+ "settings.remotePairing.details.title": "Details",
522
+ "settings.remotePairing.pairings.title": "Allowed devices",
523
+ "settings.remotePairing.pairings.otherTitle": "Other allowed devices",
524
+ "settings.remotePairing.pairings.empty": "No other allowed devices are registered yet.",
525
+ "settings.remotePairing.pairings.added": "Added",
526
+ "settings.remotePairing.pairings.lastSeen": "Last seen",
527
+ "settings.remotePairing.pairings.never": "—",
528
+ "settings.remotePairing.pairings.live": "Live",
529
+ "settings.remotePairing.pairings.offline": "Offline",
530
+ "settings.remotePairing.pairings.revoke": "Revoke",
531
+ "settings.remotePairing.pairings.revokeConfirm": "Revoke this paired phone? It will be cut off immediately and will need to re-pair on LAN to reconnect.",
532
+ "settings.remotePairing.notice.toggleOn": "Remote relay is now ON.",
533
+ "settings.remotePairing.notice.toggleOff": "Remote relay is now OFF.",
534
+ "settings.remotePairing.notice.relayUrlSaved": "Relay URL saved.",
535
+ "settings.remotePairing.notice.revoked": "Pairing revoked.",
536
+ "settings.remotePairing.notice.tokenRotated": "Connection key updated.",
537
+ "settings.remotePairing.error.toggleFailed": "Couldn't change the relay state. Please try again.",
538
+ "settings.remotePairing.error.relayUrlFailed": "Couldn't save the relay URL.",
539
+ "settings.remotePairing.error.invalidRelayUrl": "Relay URL must use wss://. ws:// is allowed only for localhost testing.",
540
+ "settings.remotePairing.error.revokeFailed": "Couldn't revoke that pairing.",
541
+ "settings.remotePairing.error.tokenRotateFailed": "Couldn't update the connection key. Open viveworker on the same Wi-Fi and try again.",
542
+ "settings.remotePairing.pairings.thisDevice": "This device",
543
+ "settings.remotePairing.thisDevice.title": "This device",
544
+ "settings.remotePairing.thisDevice.registeredTitle": "This iPhone is registered",
545
+ "settings.remotePairing.thisDevice.notEnrolledTitle": "This iPhone is not registered",
546
+ "settings.remotePairing.thisDevice.copy": "It can connect to the PC from outside the local LAN when remote connection is ON.",
547
+ "settings.remotePairing.thisDevice.notEnrolled": "Re-pair on the same LAN to register this device for remote connection.",
548
+ "settings.remotePairing.thisDevice.fingerprint": "Phone fingerprint",
549
+ "settings.remotePairing.token.rotate": "Update connection key",
550
+ "settings.remotePairing.token.lanOnly": "Open viveworker on the same Wi-Fi as the PC to update this key.",
551
+ "settings.remotePairing.audit.title": "Connection history",
552
+ "settings.remotePairing.audit.empty": "No remote connection events yet.",
553
+ "settings.remotePairing.audit.type.relay_enabled": "Remote connection turned on",
554
+ "settings.remotePairing.audit.type.relay_disabled": "Remote connection turned off",
555
+ "settings.remotePairing.audit.type.relay_url_changed": "Relay URL changed",
556
+ "settings.remotePairing.audit.type.pairing_enrolled": "Device registered",
557
+ "settings.remotePairing.audit.type.pairing_refreshed": "Device registration refreshed",
558
+ "settings.remotePairing.audit.type.pairing_revoked": "Device revoked",
559
+ "settings.remotePairing.audit.type.token_rotated": "Connection key updated",
560
+ "settings.remotePairing.audit.type.token_auto_rotated": "Connection key refreshed on LAN",
561
+ "settings.remotePairing.audit.type.token_rotate_denied": "Connection key update blocked",
562
+ "settings.remotePairing.audit.type.token_rotate_failed": "Connection key update failed",
563
+ "settings.remotePairing.audit.type.session_connected": "Encrypted connection established",
564
+ "settings.remotePairing.audit.type.session_disconnected": "Remote connection ended",
565
+ "settings.remotePairing.audit.type.session_failed": "Remote connection failed",
566
+ "settings.remotePairing.audit.type.session_error": "Remote connection error",
567
+ "settings.remotePairing.audit.type.unknown": "Remote connection event",
480
568
  "a2a.task.eyebrow": "A2A Task Request",
481
569
  "a2a.task.from": "From",
482
570
  "a2a.task.instruction": "Task",
@@ -520,6 +608,7 @@ const translations = {
520
608
  "settings.wallet.flow.title": "Recommended order",
521
609
  "settings.wallet.flow.banner.title": "Let's set up your wallet",
522
610
  "settings.wallet.flow.copy": "You need to activate your wallet to exchange task rewards over A2A.",
611
+ "settings.wallet.betaNotice": "Wallet is in beta. During this test period, wallet payments and payouts are available on testnets only.",
523
612
  "settings.wallet.sessionExpired.title": "Refresh your wallet session",
524
613
  "settings.wallet.sessionExpired.copy": "Your wallet session expired. Sign in again with a one-time password; your passkey and Base Sepolia address stay linked.",
525
614
  "settings.wallet.ready.title": "Ready to use",
@@ -533,6 +622,7 @@ const translations = {
533
622
  "settings.wallet.status.pending": "Pending",
534
623
  "settings.wallet.status.locked": "Waiting",
535
624
  "settings.wallet.status.optional": "Optional",
625
+ "settings.wallet.status.comingSoon": "Coming soon",
536
626
  "settings.wallet.step.signIn.title": "Sign in with email",
537
627
  "settings.wallet.step.signIn.copy": "Send a one-time password to your email, then enter it here to sign in to your wallet.",
538
628
  "settings.wallet.step.refreshSession.title": "Refresh session",
@@ -542,7 +632,8 @@ const translations = {
542
632
  "settings.wallet.step.baseSepolia.title": "Issue a Base Sepolia wallet",
543
633
  "settings.wallet.step.baseSepolia.copy": "Create a testing wallet for the current beta flow. This is the main wallet we recommend issuing first.",
544
634
  "settings.wallet.step.base.title": "Issue a Base wallet",
545
- "settings.wallet.step.base.copy": "Create a Base mainnet wallet when you are ready for production payouts. This step is optional for now.",
635
+ "settings.wallet.step.base.copy": "Base mainnet wallet issuance is planned for production payouts. It is not active yet.",
636
+ "settings.wallet.step.base.comingSoonDetail": "Coming soon",
546
637
  "settings.wallet.mainnet.optIn": "Also set up Base mainnet",
547
638
  "settings.wallet.mainnet.optInHint": "Optional — required only for production payouts.",
548
639
  "settings.hazbase.title": "Wallet",
@@ -616,6 +707,8 @@ const translations = {
616
707
  "notice.loggedOutDeviceRemoved": "Logged out and removed this device. Pair again before using viveworker here.",
617
708
  "error.networkHint":
618
709
  "Try reopening the latest HTTPS URL in your browser, then re-add the Home Screen app if needed.",
710
+ "error.remotePairingNeedsLanRefresh": "Remote connection needs a one-time refresh. Open viveworker once on the same Wi-Fi as your PC, then try off-LAN again.",
711
+ "error.remotePairingUnavailable": "Remote connection is off or the PC is not reachable. Turn on Remote connection on the same Wi-Fi, then try again.",
619
712
  "error.webPushDisabled": "Web Push is not enabled on the server.",
620
713
  "error.notificationsRequireHttps": "Notifications require HTTPS.",
621
714
  "error.pushUnsupported": "This browser does not support Web Push.",
@@ -707,6 +800,8 @@ const translations = {
707
800
  "claudeAway.notifyOnly.notice": "Sync mode is OFF — please respond on the desktop. {provider} is waiting for input there.",
708
801
  "server.message.approvalAccepted": "Approved. {provider} will continue.",
709
802
  "server.message.approvalRejected": "Rejected. {provider} was told not to continue this action.",
803
+ "server.message.paymentSubmitted": "Payment submitted. Waiting for settlement to complete.",
804
+ "server.message.paymentFailed": "Payment failed: {reason}",
710
805
  "server.message.planImplemented": "{provider} was asked to start implementing from this plan.",
711
806
  "server.message.planDismissed": "This plan prompt was closed and future reminders were suppressed.",
712
807
  "server.confirm.planImplement": "Implementation will start from this plan.",
@@ -828,8 +923,8 @@ const translations = {
828
923
  "Open the HTTP URL in a mobile browser only if you intentionally allowed insecure LAN access. This mode is not recommended on shared Wi-Fi.",
829
924
  "cli.setup.instructions.localOnlyHttp":
830
925
  "This setup is loopback-only HTTP for local development. Use the default HTTPS setup or --allow-insecure-http-lan if you explicitly need LAN access.",
831
- "logout.confirm.title": "Choose how to log out",
832
- "logout.confirm.copy": "You can end just this session, or remove trust for this device so pairing is required next time.",
926
+ "logout.confirm.title": "Log out of viveworker?",
927
+ "logout.confirm.copy": "This ends the current session. This device stays trusted, so you can come back without pairing again.",
833
928
  "logout.confirm.keepTrustedTitle": "Log out only",
834
929
  "logout.confirm.keepTrustedCopy": "End this session now, but keep this device trusted for the next time you open viveworker.",
835
930
  "logout.confirm.removeTitle": "Log out and remove this device",
@@ -946,6 +1041,7 @@ const translations = {
946
1041
  "common.completed": "完了",
947
1042
  "common.clear": "クリア",
948
1043
  "common.cancel": "キャンセル",
1044
+ "common.close": "閉じる",
949
1045
  "common.loading": "読み込み中",
950
1046
  "common.unavailable": "利用不可",
951
1047
  "common.yes": "はい",
@@ -1033,6 +1129,12 @@ const translations = {
1033
1129
  "このアプリの通知は現在ブロックされています。あとで端末の設定から再度有効にできます。",
1034
1130
  "banner.push.copy.default":
1035
1131
  "バックグラウンドでも承認、プラン、質問、完了に気づけるよう、プッシュ通知を有効にしてください。",
1132
+ "banner.clientUpdate.title": "この端末の viveworker を更新してください",
1133
+ "banner.clientUpdate.copy":
1134
+ "PC 側の viveworker が新しくなっています。同じ Wi-Fi または USB 接続でこの端末を開き直すと、リモート接続でも最新のコードを使えます。",
1135
+ "banner.clientUpdate.action": "更新して再読み込み",
1136
+ "error.clientUpdateNeedsLan":
1137
+ "PC と同じ Wi-Fi または USB 接続で開いてから、もう一度更新してください。",
1036
1138
  "install.guide.title": "ホーム画面に追加",
1037
1139
  "install.guide.intro.safari":
1038
1140
  "このブラウザではサイト側から直接追加できませんが、数タップでホーム画面に追加できます。",
@@ -1076,6 +1178,8 @@ const translations = {
1076
1178
  "detail.autoPilot.writeSourceContinuity": "小さなコード修正は、このスレッド内で同じファイルを直前に読んでいることが必要です。",
1077
1179
  "detail.autoPilot.writeNoLaneMatch": "この変更は、現在のワークスペースで有効な低リスク書き込みレーンに当てはまりませんでした。",
1078
1180
  "detail.imageAlt": ({ index }) => `添付画像 ${index}`,
1181
+ "detail.imageUnavailable": "画像を表示できません",
1182
+ "detail.imageUnavailableHint": "Mac 側のファイル権限が必要です",
1079
1183
  "detail.filesTitle": "関連ファイル",
1080
1184
  "detail.diffTitle": "差分",
1081
1185
  "detail.diffUnavailable": "この変更の差分はまだ利用できません。対象ファイルのみ表示します。",
@@ -1099,6 +1203,8 @@ const translations = {
1099
1203
  "reply.eyebrow": "返信",
1100
1204
  "reply.title": "{provider} に返答する",
1101
1205
  "reply.copy": "この完了結果を見ながら、同じ {provider} スレッドに新しいメッセージを送れます。",
1206
+ "reply.dockHint": "結果を見たまま、下から返答を書けます。",
1207
+ "reply.draftHint": "下書きがあります",
1102
1208
  "reply.fieldLabel": "メッセージ",
1103
1209
  "reply.placeholder": "続きを依頼したり、結果の修正や別案を {provider} に伝えます。",
1104
1210
  "reply.imageLabel": "画像",
@@ -1167,6 +1273,7 @@ const translations = {
1167
1273
  "timeline.filterLabel": "スレッドフィルター",
1168
1274
  "timeline.status.approved": "承認済み",
1169
1275
  "timeline.status.rejected": "拒否済み",
1276
+ "timeline.status.failed": "失敗",
1170
1277
  "timeline.status.implemented": "実装開始",
1171
1278
  "timeline.status.dismissed": "見送り",
1172
1279
  "timeline.status.submitted": "送信済み",
@@ -1187,6 +1294,12 @@ const translations = {
1187
1294
  "timeline.kindFilter.moltbookComments": "返信",
1188
1295
  "timeline.kindFilter.a2aRequests": "タスク依頼",
1189
1296
  "timeline.kindFilter.a2aResults": "タスク完了",
1297
+ "boot.status.starting": "起動中...",
1298
+ "boot.status.checkingLan": "同じWi-Fi内のPCを確認中...",
1299
+ "boot.status.switchingRemote": "リモート接続に切り替え中...",
1300
+ "boot.status.establishingRemote": "暗号化された接続を確立中...",
1301
+ "boot.status.loadingData": "データを読み込み中...",
1302
+ "boot.status.slowHint": "初回のリモート接続は数十秒かかることがあります。",
1190
1303
  "fileEvent.read": "ファイルを確認",
1191
1304
  "fileEvent.write": "ファイルを更新",
1192
1305
  "fileEvent.create": "ファイルを作成",
@@ -1229,6 +1342,7 @@ const translations = {
1229
1342
  "settings.nextStep.test.title": "準備は完了です",
1230
1343
  "settings.nextStep.test.copy": "テスト通知を送って、この端末が viveworker の push を受け取れるか確認できます。",
1231
1344
  "settings.status.notAvailable": "未対応",
1345
+ "settings.status.supported": "対応",
1232
1346
  "settings.status.unsupported": "非対応",
1233
1347
  "settings.status.needsHttps": "HTTPS が必要",
1234
1348
  "settings.status.needsHomeScreen": "ホーム画面追加が必要",
@@ -1238,6 +1352,7 @@ const translations = {
1238
1352
  "settings.status.installed": "追加済み",
1239
1353
  "settings.status.notInstalled": "未追加",
1240
1354
  "settings.status.connected": "接続済み",
1355
+ "settings.status.disconnected": "未接続",
1241
1356
  "settings.status.enabled": "有効",
1242
1357
  "settings.status.disabled": "無効",
1243
1358
  "settings.status.info": "情報",
@@ -1289,18 +1404,17 @@ const translations = {
1289
1404
  "settings.action.sendTest": "テスト通知を送信",
1290
1405
  "settings.action.reviewTechnical": "技術情報を確認",
1291
1406
  "settings.action.revokeDevice": "削除",
1292
- "settings.action.removeThisDevice": "この端末を削除",
1293
1407
  "settings.install.title": "ホーム画面に追加",
1294
1408
  "settings.install.copy":
1295
1409
  "いちばん使いやすい体験にするには、viveworker をホーム画面に追加して、インストール版を開いてください。",
1296
- "settings.device.title": "Devices",
1410
+ "settings.device.title": "信頼済み端末",
1297
1411
  "settings.device.copy": "この LAN 上で viveworker を開ける信頼済み端末を管理します。",
1298
1412
  "settings.device.count": ({ count }) => `${count} 台の端末`,
1299
1413
  "settings.device.section.current": "この端末",
1300
1414
  "settings.device.section.other": "他の信頼済み端末",
1301
1415
  "settings.device.thisDevice": "この端末",
1302
- "settings.device.emptyCurrent": "この端末は現在の有効な信頼済み端末一覧には入っていません。以前の信頼期限が切れたか、削除された可能性があります。必要なら Mac で `npx viveworker pair` を実行して再ペアリングしてください。",
1303
- "settings.device.emptyOther": "他に有効な信頼済み端末はありません。期限切れまたは削除済みの端末はここには表示されません。必要なら Mac で `npx viveworker pair` を実行して再ペアリングしてください。",
1416
+ "settings.device.emptyCurrent": "このブラウザは、LAN 側の信頼済み端末としてはまだ照合できていません。リモート接続中なら、viveworker はリモートペアリングで認証しています。必要な場合だけ Mac で `npx viveworker pair` を実行してください。",
1417
+ "settings.device.emptyOther": "他に有効な信頼済み端末はありません。",
1304
1418
  "settings.device.addAnother.title": "端末を追加する",
1305
1419
  "settings.device.addAnother.heading": "新しい端末 / ブラウザを追加",
1306
1420
  "settings.device.addAnother.copy":
@@ -1329,7 +1443,7 @@ const translations = {
1329
1443
  "settings.row.status": "状態",
1330
1444
  "settings.row.pairingStatus": "ペアリング状態",
1331
1445
  "settings.row.serverWebPush": "サーバーの Web Push",
1332
- "settings.row.secureContext": "Secure context",
1446
+ "settings.row.secureContext": "安全な接続",
1333
1447
  "settings.row.homeScreenApp": "ホーム画面アプリ",
1334
1448
  "settings.row.notificationPermission": "通知権限",
1335
1449
  "settings.permission.granted": "付与済",
@@ -1337,7 +1451,7 @@ const translations = {
1337
1451
  "settings.permission.default": "未設定",
1338
1452
  "settings.row.browserSupport": "ブラウザ対応",
1339
1453
  "settings.row.pushStatus": "Push 通知",
1340
- "settings.row.currentDeviceSubscribed": "この端末の購読状態",
1454
+ "settings.row.currentDeviceSubscribed": "通知の受け取り",
1341
1455
  "settings.row.lastSuccessfulDelivery": "最後の通知成功時刻",
1342
1456
  "settings.row.version": "バージョン",
1343
1457
  "settings.updateAvailable.title": "新しいバージョンがあります",
@@ -1375,7 +1489,7 @@ const translations = {
1375
1489
  "moltbook.draft.greetEvening": "今日もお疲れ様でした!成果をシェアしませんか?",
1376
1490
  "moltbook.draft.greetFallback": "投稿ドラフトができました",
1377
1491
  "settings.group.integrations": "連携サービス",
1378
- "settings.a2aRelay.title": "A2A Relay",
1492
+ "settings.a2aRelay.title": "エージェント連携",
1379
1493
  "settings.a2aRelay.subtitle": "Agent-to-Agent リレー接続",
1380
1494
  "settings.a2aRelay.copy": "A2A リレーの接続状態と設定を確認できます。",
1381
1495
  "settings.a2aRelay.unavailable": "A2A Relay が設定されていません。",
@@ -1394,6 +1508,75 @@ const translations = {
1394
1508
  "settings.row.a2aTaskCompleted": "完了タスク",
1395
1509
  "settings.row.a2aTaskDenied": "拒否タスク",
1396
1510
  "settings.a2aRelay.taskStats.title": "タスク履歴",
1511
+ "settings.remotePairing.title": "リモート接続",
1512
+ "settings.remotePairing.subtitle": "Wi-Fi 外でもペア済み端末から操作",
1513
+ "settings.remotePairing.copy": "外出先でも、ペア済み端末から PC の viveworker に接続できます。",
1514
+ "settings.remotePairing.connection.title": "リモート接続",
1515
+ "settings.remotePairing.connection.copy": "PC がリレー経由の暗号化接続を待ち受けます。",
1516
+ "settings.remotePairing.security.copy": "接続できるのはペア済み端末だけです。通信内容はエンドツーエンドで暗号化されます。",
1517
+ "settings.remotePairing.toggle.title": "リモート接続",
1518
+ "settings.remotePairing.toggle.description": "ON にすると、PC がリレー経由の暗号化接続を待ち受けます。OFF にすると LAN 内からの接続だけに戻ります。",
1519
+ "settings.remotePairing.toggle.offConfirm": "リモート接続を OFF にしますか? Wi-Fi 外の端末からは PC に接続できなくなります。再度 ON にするには、PC と同じ Wi-Fi で viveworker を開く必要があります。",
1520
+ "settings.remotePairing.status.title": "現在の状態",
1521
+ "settings.remotePairing.status.disabled": "オフ",
1522
+ "settings.remotePairing.status.waiting": "待機中",
1523
+ "settings.remotePairing.status.available": "接続可能",
1524
+ "settings.remotePairing.status.connected": "接続中",
1525
+ "settings.remotePairing.status.connecting": "待機中…",
1526
+ "settings.remotePairing.relayUrl.title": "リレー URL",
1527
+ "settings.remotePairing.relayUrl.placeholder": "wss://pairing.viveworker.com",
1528
+ "settings.remotePairing.relayUrl.help": "空欄でデフォルトを使用。通常は wss:// を使い、ws:// は localhost のテスト時だけ使えます。",
1529
+ "settings.remotePairing.relayUrl.save": "保存",
1530
+ "settings.remotePairing.identity.title": "ブリッジ ID",
1531
+ "settings.remotePairing.identity.fingerprint": "フィンガープリント",
1532
+ "settings.remotePairing.identity.empty": "(初回有効化時に生成)",
1533
+ "settings.remotePairing.details.title": "詳細設定",
1534
+ "settings.remotePairing.pairings.title": "許可済み端末",
1535
+ "settings.remotePairing.pairings.otherTitle": "他の許可済み端末",
1536
+ "settings.remotePairing.pairings.empty": "他の許可済み端末はありません。",
1537
+ "settings.remotePairing.pairings.added": "追加",
1538
+ "settings.remotePairing.pairings.lastSeen": "最終接続",
1539
+ "settings.remotePairing.pairings.never": "—",
1540
+ "settings.remotePairing.pairings.live": "オンライン",
1541
+ "settings.remotePairing.pairings.offline": "オフライン",
1542
+ "settings.remotePairing.pairings.revoke": "失効",
1543
+ "settings.remotePairing.pairings.revokeConfirm": "このペア済み端末を失効しますか? 即座に切断され、再接続には LAN 経由での再ペアが必要になります。",
1544
+ "settings.remotePairing.notice.toggleOn": "リモートリレーを ON にしました。",
1545
+ "settings.remotePairing.notice.toggleOff": "リモートリレーを OFF にしました。",
1546
+ "settings.remotePairing.notice.relayUrlSaved": "リレー URL を保存しました。",
1547
+ "settings.remotePairing.notice.revoked": "ペアリングを失効しました。",
1548
+ "settings.remotePairing.notice.tokenRotated": "接続キーを更新しました。",
1549
+ "settings.remotePairing.error.toggleFailed": "リレー状態を変更できませんでした。再試行してください。",
1550
+ "settings.remotePairing.error.relayUrlFailed": "リレー URL を保存できませんでした。",
1551
+ "settings.remotePairing.error.invalidRelayUrl": "リレー URL は wss:// を使ってください。ws:// は localhost のテスト時だけ使えます。",
1552
+ "settings.remotePairing.error.revokeFailed": "ペアリングを失効できませんでした。",
1553
+ "settings.remotePairing.error.tokenRotateFailed": "接続キーを更新できませんでした。同じ Wi-Fi で viveworker を開いて再試行してください。",
1554
+ "settings.remotePairing.pairings.thisDevice": "この端末",
1555
+ "settings.remotePairing.thisDevice.title": "この端末",
1556
+ "settings.remotePairing.thisDevice.registeredTitle": "この iPhone は登録済み",
1557
+ "settings.remotePairing.thisDevice.notEnrolledTitle": "この iPhone は未登録",
1558
+ "settings.remotePairing.thisDevice.copy": "リモート接続が ON の時、LAN 外から PC に接続できます。",
1559
+ "settings.remotePairing.thisDevice.notEnrolled": "同じ LAN で再ペアリングすると、この端末がリモート接続に登録されます。",
1560
+ "settings.remotePairing.thisDevice.fingerprint": "端末フィンガープリント",
1561
+ "settings.remotePairing.token.rotate": "接続キーを更新",
1562
+ "settings.remotePairing.token.lanOnly": "接続キーの更新は、PC と同じ Wi-Fi で開いている時に実行できます。",
1563
+ "settings.remotePairing.audit.title": "接続履歴",
1564
+ "settings.remotePairing.audit.empty": "リモート接続の履歴はまだありません。",
1565
+ "settings.remotePairing.audit.type.relay_enabled": "リモート接続を ON",
1566
+ "settings.remotePairing.audit.type.relay_disabled": "リモート接続を OFF",
1567
+ "settings.remotePairing.audit.type.relay_url_changed": "リレー URL を変更",
1568
+ "settings.remotePairing.audit.type.pairing_enrolled": "端末を登録",
1569
+ "settings.remotePairing.audit.type.pairing_refreshed": "端末登録を更新",
1570
+ "settings.remotePairing.audit.type.pairing_revoked": "端末を失効",
1571
+ "settings.remotePairing.audit.type.token_rotated": "接続キーを更新",
1572
+ "settings.remotePairing.audit.type.token_auto_rotated": "LAN で接続キーを自動更新",
1573
+ "settings.remotePairing.audit.type.token_rotate_denied": "接続キー更新をブロック",
1574
+ "settings.remotePairing.audit.type.token_rotate_failed": "接続キー更新に失敗",
1575
+ "settings.remotePairing.audit.type.session_connected": "暗号化接続を確立",
1576
+ "settings.remotePairing.audit.type.session_disconnected": "リモート接続を終了",
1577
+ "settings.remotePairing.audit.type.session_failed": "リモート接続に失敗",
1578
+ "settings.remotePairing.audit.type.session_error": "リモート接続エラー",
1579
+ "settings.remotePairing.audit.type.unknown": "リモート接続イベント",
1397
1580
  "a2a.task.eyebrow": "A2A タスクリクエスト",
1398
1581
  "a2a.task.from": "送信元",
1399
1582
  "a2a.task.instruction": "タスク内容",
@@ -1413,7 +1596,7 @@ const translations = {
1413
1596
  "settings.a2aExecutor.ask": "毎回確認する",
1414
1597
  "settings.a2aExecutor.unavailable": "未検出",
1415
1598
  "settings.a2aExecutor.detected": "検出",
1416
- "settings.a2aShare.title": "File Share",
1599
+ "settings.a2aShare.title": "ファイル共有",
1417
1600
  "settings.a2aShare.subtitle": "ファイルホスティング",
1418
1601
  "settings.a2aShare.copy": "静的ファイル (HTML / PDF / 画像 / CSV) を非公開 URL でホストします。A2A と認証情報を共有するため追加の設定は不要です。",
1419
1602
  "settings.a2aShare.unavailable": "File Share が設定されていません。`viveworker a2a setup` を実行して認証情報を設定してください。",
@@ -1429,7 +1612,7 @@ const translations = {
1429
1612
  "settings.a2aShare.expired": "期限切れ",
1430
1613
  "settings.a2aShare.status.unreachable": "到達不可",
1431
1614
  "settings.a2aShare.error": "上流への問い合わせに失敗しました: {reason}",
1432
- "settings.wallet.title": "Wallet",
1615
+ "settings.wallet.title": "ウォレット",
1433
1616
  "settings.wallet.subtitle": "サインイン / Passkey / Base アドレス",
1434
1617
  "settings.wallet.copy": "ウォレットへのサインイン、Passkey、Base 系アドレスの管理を行います。",
1435
1618
  "settings.wallet.unavailable": "この viveworker では Wallet が設定されていません。",
@@ -1437,6 +1620,7 @@ const translations = {
1437
1620
  "settings.wallet.flow.title": "おすすめの順番",
1438
1621
  "settings.wallet.flow.banner.title": "まずは、セットアップしましょう",
1439
1622
  "settings.wallet.flow.copy": "A2A でタスクの報酬をやりとりするには、ウォレットを有効化する必要があります。",
1623
+ "settings.wallet.betaNotice": "ウォレット機能はベータテスト中です。この期間はテストネットでのみ支払い・受け取りを利用できます。",
1440
1624
  "settings.wallet.sessionExpired.title": "ウォレットセッションを更新しましょう",
1441
1625
  "settings.wallet.sessionExpired.copy": "ウォレットのセッションが切れています。ワンタイムパスワードで再サインインすれば、Passkey と Base Sepolia アドレスはそのまま使えます。",
1442
1626
  "settings.wallet.ready.title": "利用準備ができました",
@@ -1450,6 +1634,7 @@ const translations = {
1450
1634
  "settings.wallet.status.pending": "保留",
1451
1635
  "settings.wallet.status.locked": "前の step 待ち",
1452
1636
  "settings.wallet.status.optional": "任意",
1637
+ "settings.wallet.status.comingSoon": "Coming soon",
1453
1638
  "settings.wallet.step.signIn.title": "メールでサインイン",
1454
1639
  "settings.wallet.step.signIn.copy": "メールにワンタイムパスワードを送り、届いた値を入力してウォレットにサインインします。",
1455
1640
  "settings.wallet.step.refreshSession.title": "セッションを更新",
@@ -1459,7 +1644,8 @@ const translations = {
1459
1644
  "settings.wallet.step.baseSepolia.title": "Base Sepolia ウォレットを発行",
1460
1645
  "settings.wallet.step.baseSepolia.copy": "現在の beta フローで使うテスト用ウォレットを発行します。まずはこれを用意するのがおすすめです。",
1461
1646
  "settings.wallet.step.base.title": "Base ウォレットを発行",
1462
- "settings.wallet.step.base.copy": "本番の payout が必要になったら Base mainnet ウォレットを発行します。現時点では任意です。",
1647
+ "settings.wallet.step.base.copy": "本番 payout 向けの Base mainnet ウォレット発行は準備中です。まだ有効化されていません。",
1648
+ "settings.wallet.step.base.comingSoonDetail": "Coming soon",
1463
1649
  "settings.wallet.mainnet.optIn": "Base mainnet も設定する",
1464
1650
  "settings.wallet.mainnet.optInHint": "任意 — 本番 payout が必要な場合だけ。",
1465
1651
  "settings.hazbase.title": "ウォレット",
@@ -1533,6 +1719,8 @@ const translations = {
1533
1719
  "notice.loggedOutDeviceRemoved": "ログアウトし、この端末の信頼も削除しました。次回は再 pairing が必要です。",
1534
1720
  "error.networkHint":
1535
1721
  "最新の HTTPS URL をブラウザで開き直して、必要ならホーム画面アプリを追加し直してください。",
1722
+ "error.remotePairingNeedsLanRefresh": "リモート接続の更新が必要です。一度 PC と同じ WiFi で viveworker を開いてから、もう一度 LAN 外で試してください。",
1723
+ "error.remotePairingUnavailable": "リモート接続が OFF、または PC に到達できません。同じ WiFi でリモート接続を ON にしてから、もう一度試してください。",
1536
1724
  "error.webPushDisabled": "サーバー側で Web Push が有効ではありません。",
1537
1725
  "error.notificationsRequireHttps": "通知には HTTPS が必要です。",
1538
1726
  "error.pushUnsupported": "このブラウザは Web Push に対応していません。",
@@ -1624,6 +1812,8 @@ const translations = {
1624
1812
  "claudeAway.notifyOnly.notice": "同期モードは OFF です。PC で操作してください。{provider} は PC からの入力を待っています。",
1625
1813
  "server.message.approvalAccepted": "承認しました。{provider} を続行します。",
1626
1814
  "server.message.approvalRejected": "拒否しました。{provider} にこの操作を続けないよう伝えました。",
1815
+ "server.message.paymentSubmitted": "支払いを送信しました。決済完了を待っています。",
1816
+ "server.message.paymentFailed": "支払いに失敗しました: {reason}",
1627
1817
  "server.message.planImplemented": "この plan から実装を開始するよう {provider} に伝えました。",
1628
1818
  "server.message.planDismissed": "この Plan 確認を閉じ、再通知を抑止しました。",
1629
1819
  "server.confirm.planImplement": "この plan から実装を開始します。",
@@ -1745,8 +1935,8 @@ const translations = {
1745
1935
  "HTTP URL は、LAN 上の平文アクセスを明示的に許可した場合だけブラウザで開いてください。共有 Wi-Fi では非推奨です。",
1746
1936
  "cli.setup.instructions.localOnlyHttp":
1747
1937
  "この setup はローカル開発用の loopback-only HTTP です。別の端末から使うなら既定の HTTPS setup か --allow-insecure-http-lan を使ってください。",
1748
- "logout.confirm.title": "ログアウト方法を選んでください",
1749
- "logout.confirm.copy": "session だけ終えることも、この端末の信頼ごと削除して次回 pairing 必須にすることもできます。",
1938
+ "logout.confirm.title": "viveworker からログアウトしますか?",
1939
+ "logout.confirm.copy": "現在の session を終了します。この端末の信頼は維持されるので、次回は再 pairing なしで戻れます。",
1750
1940
  "logout.confirm.keepTrustedTitle": "ログアウトのみ",
1751
1941
  "logout.confirm.keepTrustedCopy": "今の session だけ終了し、この端末の信頼は残します。",
1752
1942
  "logout.confirm.removeTitle": "ログアウトしてこの端末を削除",
package/web/index.html CHANGED
@@ -8,10 +8,124 @@
8
8
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
9
9
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
10
10
  <link rel="stylesheet" href="/app.css">
11
+ <style>
12
+ .boot-splash {
13
+ position: fixed;
14
+ inset: 0;
15
+ z-index: 9999;
16
+ display: grid;
17
+ place-items: center;
18
+ padding: max(1.2rem, env(safe-area-inset-top)) 1rem max(1.2rem, env(safe-area-inset-bottom));
19
+ color: #f5fbff;
20
+ background:
21
+ radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%),
22
+ radial-gradient(circle at 78% 78%, rgba(79, 131, 216, 0.14), transparent 28%),
23
+ linear-gradient(180deg, #081015 0%, #091015 100%);
24
+ transition: opacity 220ms ease, visibility 220ms ease;
25
+ }
26
+ .boot-splash__card {
27
+ width: min(20rem, 82vw);
28
+ display: grid;
29
+ justify-items: center;
30
+ gap: 0.9rem;
31
+ text-align: center;
32
+ }
33
+ .boot-splash__logo {
34
+ width: clamp(5.4rem, 28vw, 7rem);
35
+ height: clamp(5.4rem, 28vw, 7rem);
36
+ border-radius: 28%;
37
+ background:
38
+ radial-gradient(circle at 76% 24%, rgba(125, 211, 252, 0.22), transparent 30%),
39
+ linear-gradient(180deg, rgba(23, 52, 72, 0.96), rgba(9, 17, 23, 0.96));
40
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
41
+ }
42
+ .boot-splash__title {
43
+ margin: 0.25rem 0 0;
44
+ font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
45
+ font-size: clamp(1.65rem, 8vw, 2.4rem);
46
+ line-height: 1;
47
+ letter-spacing: -0.04em;
48
+ }
49
+ .boot-splash__status {
50
+ margin: 0;
51
+ color: rgba(205, 220, 231, 0.72);
52
+ font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
53
+ font-size: 0.9rem;
54
+ letter-spacing: 0.02em;
55
+ min-height: 1.25em;
56
+ transition: opacity 160ms ease, transform 160ms ease;
57
+ }
58
+ .boot-splash__hint {
59
+ max-width: 15rem;
60
+ margin: -0.35rem 0 0;
61
+ color: rgba(178, 196, 210, 0.58);
62
+ font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
63
+ font-size: 0.78rem;
64
+ line-height: 1.45;
65
+ opacity: 0;
66
+ transform: translateY(-0.2rem);
67
+ transition: opacity 220ms ease, transform 220ms ease;
68
+ }
69
+ .boot-splash__hint.is-visible {
70
+ opacity: 1;
71
+ transform: translateY(0);
72
+ }
73
+ .boot-splash__dots {
74
+ display: inline-grid;
75
+ grid-auto-flow: column;
76
+ gap: 0.34rem;
77
+ margin-top: 0.15rem;
78
+ }
79
+ .boot-splash__dots span {
80
+ width: 0.42rem;
81
+ height: 0.42rem;
82
+ border-radius: 999px;
83
+ background: rgba(142, 215, 255, 0.88);
84
+ animation: viveworker-boot-dot 980ms ease-in-out infinite;
85
+ }
86
+ .boot-splash__dots span:nth-child(2) { animation-delay: 140ms; }
87
+ .boot-splash__dots span:nth-child(3) { animation-delay: 280ms; }
88
+ .viveworker-ready .boot-splash {
89
+ opacity: 0;
90
+ visibility: hidden;
91
+ }
92
+ @keyframes viveworker-boot-dot {
93
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.42; }
94
+ 40% { transform: translateY(-0.28rem); opacity: 1; }
95
+ }
96
+ @media (prefers-reduced-motion: reduce) {
97
+ .boot-splash,
98
+ .boot-splash__status,
99
+ .boot-splash__hint,
100
+ .boot-splash__dots span {
101
+ transition: none;
102
+ animation: none;
103
+ }
104
+ }
105
+ </style>
11
106
  <title>viveworker</title>
12
107
  </head>
13
108
  <body>
109
+ <div id="boot-splash" class="boot-splash" role="status" aria-live="polite" aria-label="viveworker is starting">
110
+ <div class="boot-splash__card">
111
+ <img class="boot-splash__logo" src="/icons/viveworker-v-pulse.svg" alt="" width="112" height="112" decoding="async">
112
+ <h1 class="boot-splash__title">viveworker</h1>
113
+ <p id="boot-splash-status" class="boot-splash__status">Checking your trusted Wi-Fi...</p>
114
+ <p id="boot-splash-hint" class="boot-splash__hint" hidden>The first remote connection can take tens of seconds.</p>
115
+ <span class="boot-splash__dots" aria-hidden="true"><span></span><span></span><span></span></span>
116
+ </div>
117
+ </div>
14
118
  <div id="app"></div>
15
- <script type="module" src="/app.js"></script>
119
+ <script>
120
+ (() => {
121
+ const isJa = (navigator.language || "").toLowerCase().startsWith("ja");
122
+ const message = isJa ? "同じWi-Fi内のPCを確認中..." : "Checking your trusted Wi-Fi...";
123
+ const status = document.getElementById("boot-splash-status");
124
+ const splash = document.getElementById("boot-splash");
125
+ if (status) status.textContent = message;
126
+ if (splash) splash.setAttribute("aria-label", `viveworker ${message}`);
127
+ })();
128
+ </script>
129
+ <script type="module" src="/app.js?v=20260428-remote-token-refresh"></script>
16
130
  </body>
17
131
  </html>