viveworker 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,8 @@ Use these commands most often:
163
163
  start `viveworker` again using the saved config
164
164
  - `npx viveworker stop`
165
165
  stop the local background service
166
+ - `npx viveworker restart`
167
+ restart the local bridge using the saved config
166
168
  - `npx viveworker status`
167
169
  show the current app URL, launchd/background status, and health
168
170
  - `npx viveworker doctor`
@@ -266,6 +268,7 @@ What it supports:
266
268
  - PNG / JPG / GIF / WebP
267
269
  - CSV rendered as an HTML table by default
268
270
  - optional password protection
271
+ - short-lived token URLs for password-protected shares
269
272
  - optional expiry
270
273
 
271
274
  HTML uploads are optimized by default when possible.
@@ -288,10 +291,17 @@ Typical commands:
288
291
  - `npx viveworker share update <slug> --file updated-report.html`
289
292
  - `npx viveworker share update <slug> --password "hunter2"`
290
293
  - `npx viveworker share update <slug> --expires-days 7`
291
- - `npx viveworker share link <slug>`
294
+ - `npx viveworker share link <slug> --password "hunter2" --ttl-hours 24`
292
295
  - `VIVEWORKER_BUYER_PRIVATE_KEY=0x... npx viveworker share pay https://share.viveworker.com/v/<slug> --output ./deliverable.pdf`
293
296
  - `npx viveworker share pay https://share.viveworker.com/v/<slug> --wallet hazbase --output ./deliverable.pdf`
294
297
 
298
+ `share link` is for agent handoff. It verifies the current password locally with
299
+ the File Share worker and returns a short-lived `https://share.viveworker.com/v/<slug>?t=<token>`
300
+ URL, so the recipient can open the share without seeing the password. The token
301
+ defaults to 24 hours, can be capped with `--ttl-hours` up to 720 hours, never
302
+ outlives the share expiry, and is invalidated when you rotate the share password
303
+ with `share update --password`.
304
+
295
305
  `share pay` is human-in-the-loop by default. EOA mode reads the x402 payment
296
306
  requirements and asks the paired device to approve before signing. `--wallet
297
307
  hazbase` instead sends the payment request to the paired device, asks for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Open mobile control surface for Codex, Claude, Thread Sharing, File Share, Moltbook, and A2A tasks on your trusted LAN.",
5
5
  "author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
6
6
  "license": "MIT",
@@ -146,6 +146,8 @@ async function callTool(params) {
146
146
  return toolOk(await toolRequestApproval(args));
147
147
  case "viveworker_share_file":
148
148
  return toolOk(await toolShareFile(args));
149
+ case "viveworker_share_link":
150
+ return toolOk(await toolShareLink(args));
149
151
  case "viveworker_thread_share":
150
152
  return toolOk(await toolThreadShare(args));
151
153
  case "viveworker_send_a2a_task":
@@ -199,6 +201,13 @@ async function toolShareFile(args) {
199
201
  const workspaceRoot = optionalString(args.workspaceRoot) || process.env.VIVEWORKER_MCP_WORKSPACE_ROOT || process.cwd();
200
202
  const file = await validateSharePath(requestedPath, workspaceRoot);
201
203
  const stat = await fs.stat(file.realPath);
204
+ const password = optionalString(args.password);
205
+ const expiresDays = optionalString(args.expiresDays ?? args["expires-days"]);
206
+ const tokenize = optionalBoolean(args.tokenize) || optionalBoolean(args.tokenizedUrl) || optionalBoolean(args.passwordlessUrl);
207
+ const tokenTtlHours = normalizeShareTokenTtlHours(args.tokenTtlHours ?? args["token-ttl-hours"] ?? args.ttlHours ?? args["ttl-hours"]);
208
+ if (tokenize && !password) {
209
+ throw rpcInvalidParams("tokenize requires password because tokenized URLs are minted from password-protected shares");
210
+ }
202
211
  const approval = await bridgeEvent({
203
212
  eventType: "approval_request",
204
213
  title: "Share file with viveworker File Share",
@@ -207,6 +216,8 @@ async function toolShareFile(args) {
207
216
  "",
208
217
  `File: ${file.displayPath}`,
209
218
  `Size: ${stat.size} bytes`,
219
+ password ? "Password gate: enabled" : "Password gate: disabled",
220
+ tokenize ? `Token URL: create short-lived passwordless ?t= URL${tokenTtlHours ? ` (${tokenTtlHours}h)` : " (24h)"}` : "",
210
221
  "",
211
222
  "Approve only if this file is safe to send outside this Mac.",
212
223
  ].join("\n"),
@@ -219,12 +230,52 @@ async function toolShareFile(args) {
219
230
  }
220
231
 
221
232
  const uploadArgs = ["share", "upload", file.realPath, "--json"];
222
- const password = optionalString(args.password);
223
- const expiresDays = optionalString(args.expiresDays ?? args["expires-days"]);
224
233
  if (password) uploadArgs.push("--password", password);
225
234
  if (expiresDays) uploadArgs.push("--expires-days", expiresDays);
226
235
  const upload = await runViveworkerCliJson(uploadArgs, 90_000);
227
- return { approved: true, share: upload };
236
+ if (!tokenize) {
237
+ return { approved: true, share: upload };
238
+ }
239
+
240
+ const slug = optionalString(upload.slug) || slugFromShareUrl(upload.url);
241
+ if (!slug) {
242
+ throw new Error("share upload did not return a slug for tokenized URL creation");
243
+ }
244
+ const link = await createShareTokenLink(slug, password, tokenTtlHours);
245
+ return { approved: true, share: upload, tokenizedLink: link };
246
+ }
247
+
248
+ async function toolShareLink(args) {
249
+ const slug = requiredString(args.slug, "slug");
250
+ if (!/^[A-Za-z0-9]+$/.test(slug)) {
251
+ throw rpcInvalidParams("slug must contain only letters and numbers");
252
+ }
253
+ const password = requiredString(args.password, "password");
254
+ if (password.length > 256) {
255
+ throw rpcInvalidParams("password is too long (max 256 characters)");
256
+ }
257
+ const ttlHours = normalizeShareTokenTtlHours(args.ttlHours ?? args["ttl-hours"] ?? args.tokenTtlHours ?? args["token-ttl-hours"]);
258
+ const approval = await bridgeEvent({
259
+ eventType: "approval_request",
260
+ title: "Create File Share token link",
261
+ message: [
262
+ "An MCP client wants to mint a short-lived passwordless File Share URL.",
263
+ "",
264
+ `Share slug: ${slug}`,
265
+ `Token TTL: ${ttlHours || 24}h`,
266
+ "",
267
+ "Anyone with the returned ?t= URL can open this share until the token or share expires.",
268
+ "Approve only if you are comfortable handing off this password-protected share.",
269
+ ].join("\n"),
270
+ approvalKind: "file_share_link",
271
+ timeoutMs: clampTimeout(args.timeoutMs),
272
+ }, clampTimeout(args.timeoutMs));
273
+ if (!approval.approved) {
274
+ return { approved: false, decision: approval.decision || "rejected" };
275
+ }
276
+
277
+ const link = await createShareTokenLink(slug, password, ttlHours);
278
+ return { approved: true, link };
228
279
  }
229
280
 
230
281
  async function toolThreadShare(args) {
@@ -577,6 +628,43 @@ function normalizeStringArray(value) {
577
628
  return value.map((item) => String(item || "").trim()).filter(Boolean);
578
629
  }
579
630
 
631
+ function optionalBoolean(value) {
632
+ if (value === true) return true;
633
+ if (value === false || value == null) return false;
634
+ const text = String(value).trim().toLowerCase();
635
+ return text === "1" || text === "true" || text === "yes" || text === "on";
636
+ }
637
+
638
+ function normalizeShareTokenTtlHours(value) {
639
+ if (value == null || value === "") return undefined;
640
+ const n = Number(value);
641
+ if (!Number.isFinite(n) || n <= 0 || n > 720) {
642
+ throw rpcInvalidParams("token TTL must be a number between 1 and 720 hours");
643
+ }
644
+ return n;
645
+ }
646
+
647
+ function slugFromShareUrl(value) {
648
+ const text = optionalString(value);
649
+ if (!text) return "";
650
+ try {
651
+ const url = new URL(text);
652
+ const match = url.pathname.match(/^\/v\/([A-Za-z0-9]+)/u);
653
+ return match?.[1] || "";
654
+ } catch {
655
+ const match = text.match(/\/v\/([A-Za-z0-9]+)/u);
656
+ return match?.[1] || "";
657
+ }
658
+ }
659
+
660
+ async function createShareTokenLink(slug, password, ttlHours) {
661
+ const linkArgs = ["share", "link", slug, "--password", password, "--json"];
662
+ if (ttlHours !== undefined) {
663
+ linkArgs.push("--ttl-hours", String(ttlHours));
664
+ }
665
+ return runViveworkerCliJson(linkArgs, 30_000);
666
+ }
667
+
580
668
  function requiredString(value, name) {
581
669
  const text = optionalString(value);
582
670
  if (!text) throw rpcInvalidParams(`${name} is required`);
@@ -789,7 +877,7 @@ const TOOLS = [
789
877
  {
790
878
  name: "viveworker_share_file",
791
879
  title: "Share file",
792
- description: "After phone approval, upload a workspace file to viveworker File Share and return the limited URL.",
880
+ description: "After phone approval, upload a workspace file to viveworker File Share and return the limited URL. If password is set, pass tokenize=true to also mint a short-lived passwordless ?t= URL.",
793
881
  inputSchema: {
794
882
  type: "object",
795
883
  additionalProperties: false,
@@ -798,11 +886,29 @@ const TOOLS = [
798
886
  workspaceRoot: { type: "string" },
799
887
  password: { type: "string" },
800
888
  expiresDays: { type: "string" },
889
+ tokenize: { type: "boolean" },
890
+ tokenTtlHours: { type: "number" },
801
891
  timeoutMs: { type: "number" },
802
892
  },
803
893
  required: ["path"],
804
894
  },
805
895
  },
896
+ {
897
+ name: "viveworker_share_link",
898
+ title: "Create File Share token URL",
899
+ description: "After phone approval, mint a short-lived passwordless ?t= URL for an existing password-protected File Share slug.",
900
+ inputSchema: {
901
+ type: "object",
902
+ additionalProperties: false,
903
+ properties: {
904
+ slug: { type: "string" },
905
+ password: { type: "string" },
906
+ ttlHours: { type: "number" },
907
+ timeoutMs: { type: "number" },
908
+ },
909
+ required: ["slug", "password"],
910
+ },
911
+ },
806
912
  {
807
913
  name: "viveworker_thread_share",
808
914
  title: "Thread share",
@@ -871,7 +977,7 @@ const PROMPTS = [
871
977
  title: "Share deliverable",
872
978
  description: "Package a local deliverable through viveworker File Share with phone approval.",
873
979
  },
874
- text: "When the user asks to share a report, prototype, screenshot, CSV, or standalone HTML deliverable, use viveworker_share_file. Only share files inside the workspace and never share secrets or credentials.",
980
+ text: "When the user asks to share a report, prototype, screenshot, CSV, or standalone HTML deliverable, use viveworker_share_file. Only share files inside the workspace and never share secrets or credentials. If the user wants a password-protected share but the recipient should not know the password, set password plus tokenize=true, or use viveworker_share_link for an existing password-protected slug to mint a short-lived ?t= URL.",
875
981
  },
876
982
  {
877
983
  definition: {
@@ -719,7 +719,7 @@ async function handleUpdateWithFlags(flags, mode = "update") {
719
719
  // password-protected share to another agent without disclosing the password.
720
720
  //
721
721
  // The owner keeps the password on their side; the receiver only needs to GET
722
- // the returned URL. Tokens default to 24h, capped at 168h (7d) and capped by
722
+ // the returned URL. Tokens default to 24h, capped at 720h (30d) and capped by
723
723
  // the share's own `expiresAtMs`. Rotating the password via `share update
724
724
  // --password ...` invalidates every outstanding token for the slug.
725
725
  // ---------------------------------------------------------------------------
@@ -748,8 +748,8 @@ async function handleLink(args) {
748
748
  if (hasTtl) {
749
749
  const raw = flags["ttl-hours"] || flags["ttlHours"];
750
750
  const n = Number(raw);
751
- if (!Number.isFinite(n) || n <= 0 || n > 168) {
752
- throw new Error("--ttl-hours must be a number between 1 and 168");
751
+ if (!Number.isFinite(n) || n <= 0 || n > 720) {
752
+ throw new Error("--ttl-hours must be a number between 1 and 720");
753
753
  }
754
754
  ttlHours = n;
755
755
  }
@@ -1269,7 +1269,7 @@ function formatApiError(op, status, body) {
1269
1269
  case "not-password-protected":
1270
1270
  return `${op} failed (${status}): share has no password — no link token needed, just share the URL directly`;
1271
1271
  case "invalid-ttlHours":
1272
- return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours || 168}`;
1272
+ return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours || 720}`;
1273
1273
  // x402 / paid-share error codes — thrown by the worker on upload,
1274
1274
  // PATCH, or view. Keep the messages actionable; agents read these
1275
1275
  // back to the user verbatim.
@@ -115,6 +115,9 @@ async function main(cliOptions) {
115
115
  case "stop":
116
116
  await runStop(cliOptions);
117
117
  return;
118
+ case "restart":
119
+ await runRestart(cliOptions);
120
+ return;
118
121
  case "status":
119
122
  await runStatus(cliOptions);
120
123
  return;
@@ -1181,6 +1184,11 @@ async function runStop(cliOptions) {
1181
1184
  ]);
1182
1185
  }
1183
1186
 
1187
+ async function runRestart(cliOptions) {
1188
+ await runStop(cliOptions);
1189
+ await runStart(cliOptions);
1190
+ }
1191
+
1184
1192
  async function runStatus(cliOptions) {
1185
1193
  const configDir = resolvePath(cliOptions.configDir || defaultConfigDir);
1186
1194
  const envFile = resolvePath(cliOptions.envFile || path.join(configDir, "config.env"));
@@ -1834,6 +1842,7 @@ ${t(locale, "cli.help.commands")}
1834
1842
  ${t(locale, "cli.help.enable")}
1835
1843
  ${t(locale, "cli.help.start")}
1836
1844
  ${t(locale, "cli.help.stop")}
1845
+ ${t(locale, "cli.help.restart")}
1837
1846
  ${t(locale, "cli.help.status")}
1838
1847
  ${t(locale, "cli.help.doctor")}
1839
1848
  ${t(locale, "cli.help.update")}
@@ -26,7 +26,8 @@ Then restart the Claude Code session. If the tools are not available, ask the us
26
26
  - `viveworker_notify` sends an informational phone notification and records a timeline entry.
27
27
  - `viveworker_ask` asks the paired phone a question and waits for the answer.
28
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.
29
+ - `viveworker_share_file` uploads a workspace file to File Share after phone approval. For password-protected handoff, pass `tokenize: true` to return a short-lived passwordless `?t=` URL.
30
+ - `viveworker_share_link` mints a short-lived passwordless `?t=` URL for an existing password-protected File Share slug after phone approval.
30
31
  - `viveworker_thread_share` shares context into another Codex / Claude / inbox thread.
31
32
  - `viveworker_send_a2a_task` sends a task to a registered A2A target after phone approval.
32
33
 
@@ -35,6 +36,7 @@ Then restart the Claude Code session. If the tools are not available, ask the us
35
36
  - If the user says "ask me on my phone", "スマホに聞いて", or a short decision blocks progress, use `viveworker_ask`.
36
37
  - If the user asks you to proceed with a risky, external, irreversible, payment-related, or user-visible action, use `viveworker_request_approval`.
37
38
  - If the user asks for a report, prototype, screenshot, PDF, CSV, or standalone HTML to become a shareable link, use `viveworker_share_file`.
39
+ - If the user asks for a password-protected share but wants the recipient to open it without knowing the password, use `viveworker_share_file` with `password` and `tokenize: true`, or use `viveworker_share_link` for an existing slug.
38
40
  - If the user says "share this with Codex/Claude", "Aの内容をBに共有して", or wants context handed to another session, use `viveworker_thread_share`.
39
41
  - If the user wants another registered agent to do work, use `viveworker_send_a2a_task`.
40
42
 
package/web/i18n.js CHANGED
@@ -885,13 +885,14 @@ const translations = {
885
885
  "server.page.choiceMissing": "Choice input token not found",
886
886
  "server.page.approvalHandled": "Approval already handled",
887
887
  "server.page.detailMissing": "Completion detail not found",
888
- "cli.help.usage": "Usage: viveworker <setup|pair|enable|start|stop|status|doctor|update> [options]",
888
+ "cli.help.usage": "Usage: viveworker <setup|pair|enable|start|stop|restart|status|doctor|update> [options]",
889
889
  "cli.help.commands": "Commands:",
890
890
  "cli.help.setup": "setup Create config, register launchd, and start the base viveworker bridge",
891
891
  "cli.help.pair": "pair Refresh pairing info for another trusted device",
892
892
  "cli.help.enable": "enable Enable an optional integration or feature",
893
893
  "cli.help.start": "start Start the bridge using the saved config",
894
894
  "cli.help.stop": "stop Stop the bridge",
895
+ "cli.help.restart": "restart Restart the bridge using the saved config",
895
896
  "cli.help.status": "status Print launchd/background status and health",
896
897
  "cli.help.doctor": "doctor Diagnose the local setup",
897
898
  "cli.help.update": "update Update to the latest version and restart all services",
@@ -1963,13 +1964,14 @@ const translations = {
1963
1964
  "server.page.choiceMissing": "選択入力トークンが見つかりません",
1964
1965
  "server.page.approvalHandled": "この承認はすでに処理済みです",
1965
1966
  "server.page.detailMissing": "完了詳細が見つかりません",
1966
- "cli.help.usage": "Usage: viveworker <setup|pair|enable|start|stop|status|doctor|update> [options]",
1967
+ "cli.help.usage": "Usage: viveworker <setup|pair|enable|start|stop|restart|status|doctor|update> [options]",
1967
1968
  "cli.help.commands": "Commands:",
1968
1969
  "cli.help.setup": "setup base の設定を作成し、launchd に登録して viveworker bridge を起動します",
1969
1970
  "cli.help.pair": "pair 追加する信頼済み端末向けに pairing 情報を更新します",
1970
1971
  "cli.help.enable": "enable オプション機能や連携を有効化します",
1971
1972
  "cli.help.start": "start 保存済み設定で bridge を起動します",
1972
1973
  "cli.help.stop": "stop bridge を停止します",
1974
+ "cli.help.restart": "restart 保存済み設定で bridge を再起動します",
1973
1975
  "cli.help.status": "status launchd / バックグラウンド状態と health を表示します",
1974
1976
  "cli.help.doctor": "doctor ローカル設定を診断します",
1975
1977
  "cli.help.update": "update 最新バージョンに更新し、全サービスを再起動します",