viveworker 0.2.1 → 0.4.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.
@@ -181,7 +181,13 @@ async function handlePermissionRequest() {
181
181
  }
182
182
 
183
183
  const result = await postEvent("PermissionRequest", body, APPROVAL_TIMEOUT_MS);
184
- const behavior = String(result?.permissionDecision || "deny") === "allow" ? "allow" : "deny";
184
+ // If the bridge is unreachable, postEvent resolves to {} with no
185
+ // permissionDecision field. Distinguish that from an explicit deny and
186
+ // fall back to Claude Code's default permission flow instead of blocking.
187
+ if (!result || typeof result.permissionDecision !== "string") {
188
+ return;
189
+ }
190
+ const behavior = result.permissionDecision === "allow" ? "allow" : "deny";
185
191
  const reason = typeof result?.permissionDecisionReason === "string" ? result.permissionDecisionReason : "";
186
192
 
187
193
  const decision = { behavior };
@@ -27,7 +27,33 @@ const defaultLabel = "io.viveworker.app";
27
27
  const defaultTlsDir = path.join(defaultConfigDir, "tls");
28
28
  const defaultServerPort = 8810;
29
29
 
30
- const cli = parseArgs(process.argv.slice(2));
30
+ const rawArgs = process.argv.slice(2);
31
+
32
+ // `viveworker moltbook <cmd> ...` bypasses the strict parseArgs so that
33
+ // free-form flags like `--text "hello"` reach the Moltbook CLI untouched.
34
+ if (rawArgs[0] === "moltbook") {
35
+ const { runMoltbookCli } = await import("./moltbook-cli.mjs");
36
+ try {
37
+ await runMoltbookCli(rawArgs.slice(1));
38
+ process.exit(0);
39
+ } catch (error) {
40
+ console.error(error.message || String(error));
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ if (rawArgs[0] === "a2a") {
46
+ const { runA2ACli } = await import("./a2a-cli.mjs");
47
+ try {
48
+ await runA2ACli(rawArgs.slice(1));
49
+ process.exit(0);
50
+ } catch (error) {
51
+ console.error(error.message || String(error));
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ const cli = parseArgs(rawArgs);
31
57
 
32
58
  try {
33
59
  await main(cli);
@@ -300,6 +326,254 @@ async function runSetup(cliOptions) {
300
326
  console.log("");
301
327
  console.log(t(locale, "cli.setup.claudeHooksSkipped"));
302
328
  }
329
+
330
+ if (cliOptions.moltbook) {
331
+ try {
332
+ await installMoltbookWatcher({ cliOptions, sessionSecret, port });
333
+ } catch (error) {
334
+ console.log("");
335
+ console.log(`Moltbook watcher install failed: ${error.message}`);
336
+ }
337
+ }
338
+
339
+ if (cliOptions.autoScoutUninstall) {
340
+ await uninstallMoltbookScout();
341
+ } else if (cliOptions.autoScout) {
342
+ try {
343
+ await installMoltbookScout({ cliOptions });
344
+ } catch (error) {
345
+ console.log("");
346
+ console.log(`Moltbook auto-scout install failed: ${error.message}`);
347
+ }
348
+ }
349
+ }
350
+
351
+ async function detectScoutHarness(preferred) {
352
+ const { spawn } = await import("node:child_process");
353
+ const which = (cmd) =>
354
+ new Promise((resolve) => {
355
+ const p = spawn("command", ["-v", cmd], { shell: "/bin/bash", stdio: ["ignore", "pipe", "ignore"] });
356
+ let out = "";
357
+ p.stdout.on("data", (d) => (out += d.toString()));
358
+ p.on("exit", (code) => resolve(code === 0 ? out.trim() : ""));
359
+ p.on("error", () => resolve(""));
360
+ });
361
+ if (preferred === "codex") return { kind: "codex", bin: (await which("codex")) || "codex" };
362
+ if (preferred === "claude") return { kind: "claude", bin: (await which("claude")) || "claude" };
363
+ if (preferred === "manual") return { kind: "manual", bin: "" };
364
+ const codex = await which("codex");
365
+ if (codex) return { kind: "codex", bin: codex };
366
+ const claude = await which("claude");
367
+ if (claude) return { kind: "claude", bin: claude };
368
+ return { kind: "manual", bin: "" };
369
+ }
370
+
371
+ async function uninstallMoltbookScout() {
372
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${"com.viveworker.moltbook-scout"}.plist`);
373
+ const { spawn } = await import("node:child_process");
374
+ await new Promise((resolve) => {
375
+ const p = spawn("launchctl", ["bootout", `gui/${process.getuid()}`, plistPath], { stdio: "ignore" });
376
+ p.on("exit", () => resolve());
377
+ p.on("error", () => resolve());
378
+ });
379
+ try {
380
+ await fs.unlink(plistPath);
381
+ console.log("");
382
+ console.log(`Moltbook auto-scout uninstalled (removed ${plistPath}).`);
383
+ } catch {
384
+ console.log("");
385
+ console.log(`No auto-scout plist found at ${plistPath}.`);
386
+ }
387
+ }
388
+
389
+ async function installMoltbookScout({ cliOptions }) {
390
+ const moltbookEnvFile = path.join(os.homedir(), ".viveworker", "moltbook.env");
391
+ try {
392
+ await fs.access(moltbookEnvFile);
393
+ } catch {
394
+ throw new Error(
395
+ `${moltbookEnvFile} not found. Run --moltbook --moltbook-api-key ... --moltbook-agent-id ... first.`
396
+ );
397
+ }
398
+ const harness = await detectScoutHarness(cliOptions.autoScoutHarness || "auto");
399
+ const interval = Number(cliOptions.autoScoutInterval) || 120;
400
+ const scoutRunScript = path.join(packageRoot, "scripts", "moltbook-scout-run.sh");
401
+ const viveworkerJs = path.join(packageRoot, "scripts", "viveworker.mjs");
402
+ const submoltsFlag = cliOptions.autoScoutSubmolts
403
+ ? ` --submolts ${cliOptions.autoScoutSubmolts}`
404
+ : "";
405
+ const maxDailyFlag = cliOptions.autoScoutMaxDaily
406
+ ? ` --max-daily ${cliOptions.autoScoutMaxDaily}`
407
+ : "";
408
+
409
+ const personaPath = path.join(os.homedir(), ".viveworker", "moltbook-persona.md");
410
+ let hasPersona = false;
411
+ try {
412
+ const personaText = await fs.readFile(personaPath, "utf8");
413
+ hasPersona = Boolean(personaText.trim());
414
+ } catch {
415
+ // no persona file
416
+ }
417
+
418
+ // Ensure node (and harness CLI) is on PATH inside launchd's minimal env.
419
+ const nodeBinDir = path.dirname(process.execPath);
420
+ const pathPrefix = `export PATH="${nodeBinDir}:$PATH"`;
421
+ const autoScript = path.join(packageRoot, "scripts", "moltbook-scout-auto.sh");
422
+
423
+ let inner;
424
+ if (harness.kind === "manual") {
425
+ // Manual fallback: just run scout and log the candidate. No drafting.
426
+ inner =
427
+ `${pathPrefix}; set -a; . "${moltbookEnvFile}"; set +a; cd "${packageRoot}" && ` +
428
+ `"${scoutRunScript}"${submoltsFlag}${maxDailyFlag} || true`;
429
+ } else {
430
+ // Use the auto script which runs scout (no LLM), then drafts via harness, then proposes.
431
+ const envVars = [
432
+ `SCOUT_HARNESS=${harness.kind}`,
433
+ submoltsFlag ? `SCOUT_FLAGS="${submoltsFlag.trim()}${maxDailyFlag}"` : maxDailyFlag ? `SCOUT_FLAGS="${maxDailyFlag.trim()}"` : "",
434
+ ].filter(Boolean).join(" ");
435
+ inner =
436
+ `${pathPrefix}; set -a; . "${moltbookEnvFile}"; set +a; ${envVars ? envVars + " " : ""}` +
437
+ `"${autoScript}"`;
438
+ }
439
+
440
+ const userShell = process.env.SHELL || "/bin/zsh";
441
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${"com.viveworker.moltbook-scout"}.plist`);
442
+ const plistBody = `<?xml version="1.0" encoding="UTF-8"?>
443
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
444
+ <plist version="1.0">
445
+ <dict>
446
+ <key>Label</key><string>${"com.viveworker.moltbook-scout"}</string>
447
+ <key>ProgramArguments</key>
448
+ <array>
449
+ <string>${escapeXml(userShell)}</string>
450
+ <string>-lc</string>
451
+ <string>${escapeXml(inner)}</string>
452
+ </array>
453
+ <key>StartInterval</key><integer>${interval}</integer>
454
+ <key>RunAtLoad</key><false/>
455
+ <key>StandardOutPath</key><string>/tmp/viveworker-moltbook-scout.out.log</string>
456
+ <key>StandardErrorPath</key><string>/tmp/viveworker-moltbook-scout.err.log</string>
457
+ <key>ProcessType</key><string>Background</string>
458
+ </dict>
459
+ </plist>
460
+ `;
461
+ await fs.mkdir(path.dirname(plistPath), { recursive: true });
462
+ await fs.writeFile(plistPath, plistBody, "utf8");
463
+
464
+ const { spawn } = await import("node:child_process");
465
+ await new Promise((resolve) => {
466
+ const p = spawn("launchctl", ["bootout", `gui/${process.getuid()}`, plistPath], { stdio: "ignore" });
467
+ p.on("exit", () => resolve());
468
+ p.on("error", () => resolve());
469
+ });
470
+ try {
471
+ await new Promise((resolve, reject) => {
472
+ const p = spawn("launchctl", ["bootstrap", `gui/${process.getuid()}`, plistPath], { stdio: "ignore" });
473
+ p.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`launchctl bootstrap exited ${code}`))));
474
+ p.on("error", reject);
475
+ });
476
+ } catch (error) {
477
+ console.log("");
478
+ console.log(`Moltbook scout plist written but launchctl bootstrap failed: ${error.message}`);
479
+ console.log(`Run manually: launchctl bootstrap gui/$(id -u) ${plistPath}`);
480
+ }
481
+
482
+ console.log("");
483
+ console.log(`Moltbook auto-scout installed (${harness.kind}).`);
484
+ console.log(` plist: ${plistPath}`);
485
+ console.log(` interval: ${interval}s`);
486
+ if (harness.kind === "manual") {
487
+ console.log(
488
+ ` note: Neither 'codex' nor 'claude' CLI was found. The scheduled task will only run scout and print candidates to the log. Install Codex CLI (npm i -g @openai/codex) or Claude Code CLI to enable automated drafting.`
489
+ );
490
+ } else {
491
+ console.log(` harness: ${harness.bin}`);
492
+ }
493
+ console.log(` logs: /tmp/viveworker-moltbook-scout.{out,err}.log`);
494
+ if (!hasPersona) {
495
+ console.log(
496
+ ` tip: No persona file found. Run 'npx viveworker moltbook persona init' to describe your agent — draft quality improves a lot.`
497
+ );
498
+ } else {
499
+ console.log(` persona: ${personaPath}`);
500
+ }
501
+ }
502
+
503
+ async function installMoltbookWatcher({ cliOptions, sessionSecret, port }) {
504
+ if (!cliOptions.moltbookApiKey || !cliOptions.moltbookAgentId) {
505
+ throw new Error(
506
+ "--moltbook requires --moltbook-api-key and --moltbook-agent-id"
507
+ );
508
+ }
509
+ const moltbookDir = path.join(os.homedir(), ".viveworker");
510
+ const moltbookEnvFile = path.join(moltbookDir, "moltbook.env");
511
+ await fs.mkdir(moltbookDir, { recursive: true });
512
+ const envLines = [
513
+ `MOLTBOOK_API_KEY=${cliOptions.moltbookApiKey}`,
514
+ `MOLTBOOK_AGENT_ID=${cliOptions.moltbookAgentId}`,
515
+ cliOptions.moltbookAgentName ? `MOLTBOOK_AGENT_NAME=${cliOptions.moltbookAgentName}` : "",
516
+ `VIVEWORKER_HOOK_SECRET=${sessionSecret}`,
517
+ `VIVEWORKER_BASE_URL=https://127.0.0.1:${port}`,
518
+ "",
519
+ ].filter((line) => line !== null);
520
+ await fs.writeFile(moltbookEnvFile, envLines.join("\n"), { mode: 0o600 });
521
+ await fs.chmod(moltbookEnvFile, 0o600);
522
+
523
+ const plistLabel = "com.viveworker.moltbook-watcher";
524
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${plistLabel}.plist`);
525
+ const watcherScript = path.join(packageRoot, "scripts", "moltbook-watcher.mjs");
526
+ const nodePath = process.execPath;
527
+ const watcherShell = process.env.SHELL || "/bin/zsh";
528
+ const watcherNodeDir = path.dirname(nodePath);
529
+ const plistBody = `<?xml version="1.0" encoding="UTF-8"?>
530
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
531
+ <plist version="1.0">
532
+ <dict>
533
+ <key>Label</key><string>${plistLabel}</string>
534
+ <key>ProgramArguments</key>
535
+ <array>
536
+ <string>${escapeXml(watcherShell)}</string>
537
+ <string>-lc</string>
538
+ <string>export PATH="${watcherNodeDir}:$PATH"; set -a; . "${moltbookEnvFile}"; set +a; exec "${nodePath}" "${watcherScript}"</string>
539
+ </array>
540
+ <key>RunAtLoad</key><true/>
541
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/><key>Crashed</key><true/></dict>
542
+ <key>ThrottleInterval</key><integer>30</integer>
543
+ <key>StandardOutPath</key><string>/tmp/viveworker-moltbook-watcher.out.log</string>
544
+ <key>StandardErrorPath</key><string>/tmp/viveworker-moltbook-watcher.err.log</string>
545
+ <key>ProcessType</key><string>Background</string>
546
+ </dict>
547
+ </plist>
548
+ `;
549
+ await fs.mkdir(path.dirname(plistPath), { recursive: true });
550
+ await fs.writeFile(plistPath, plistBody, "utf8");
551
+
552
+ // Reload the agent if launchctl is available
553
+ try {
554
+ const { spawn } = await import("node:child_process");
555
+ await new Promise((resolve) => {
556
+ const unload = spawn("launchctl", ["unload", plistPath], { stdio: "ignore" });
557
+ unload.on("exit", () => resolve());
558
+ unload.on("error", () => resolve());
559
+ });
560
+ await new Promise((resolve, reject) => {
561
+ const load = spawn("launchctl", ["load", plistPath], { stdio: "ignore" });
562
+ load.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`launchctl load exited ${code}`))));
563
+ load.on("error", reject);
564
+ });
565
+ } catch (error) {
566
+ console.log("");
567
+ console.log(`Moltbook watcher plist written but launchctl load failed: ${error.message}`);
568
+ console.log(`Run manually: launchctl load ${plistPath}`);
569
+ return;
570
+ }
571
+
572
+ console.log("");
573
+ console.log(`Moltbook watcher installed.`);
574
+ console.log(` env: ${moltbookEnvFile}`);
575
+ console.log(` plist: ${plistPath}`);
576
+ console.log(` logs: /tmp/viveworker-moltbook-watcher.{out,err}.log`);
303
577
  }
304
578
 
305
579
  async function installClaudeHooks({ envFile, claudeSettingsFile, sessionSecret }) {
@@ -601,6 +875,16 @@ function parseArgs(argv) {
601
875
  locale: "",
602
876
  mkcertTrustStores: "",
603
877
  claudeSettingsFile: "",
878
+ moltbook: false,
879
+ moltbookApiKey: "",
880
+ moltbookAgentId: "",
881
+ moltbookAgentName: "",
882
+ autoScout: false,
883
+ autoScoutUninstall: false,
884
+ autoScoutInterval: 120,
885
+ autoScoutHarness: "auto",
886
+ autoScoutSubmolts: "",
887
+ autoScoutMaxDaily: 0,
604
888
  };
605
889
 
606
890
  if (argv[0] && !argv[0].startsWith("-")) {
@@ -682,6 +966,35 @@ function parseArgs(argv) {
682
966
  } else if (arg === "--claude-settings-file") {
683
967
  parsed.claudeSettingsFile = next;
684
968
  index += 1;
969
+ } else if (arg === "--moltbook") {
970
+ parsed.moltbook = true;
971
+ } else if (arg === "--moltbook-api-key") {
972
+ parsed.moltbook = true;
973
+ parsed.moltbookApiKey = next;
974
+ index += 1;
975
+ } else if (arg === "--moltbook-agent-id") {
976
+ parsed.moltbook = true;
977
+ parsed.moltbookAgentId = next;
978
+ index += 1;
979
+ } else if (arg === "--moltbook-agent-name") {
980
+ parsed.moltbookAgentName = next;
981
+ index += 1;
982
+ } else if (arg === "--auto-scout") {
983
+ parsed.autoScout = true;
984
+ } else if (arg === "--auto-scout-uninstall") {
985
+ parsed.autoScoutUninstall = true;
986
+ } else if (arg === "--auto-scout-interval") {
987
+ parsed.autoScoutInterval = Number(next) || 120;
988
+ index += 1;
989
+ } else if (arg === "--auto-scout-harness") {
990
+ parsed.autoScoutHarness = next;
991
+ index += 1;
992
+ } else if (arg === "--auto-scout-submolts") {
993
+ parsed.autoScoutSubmolts = next;
994
+ index += 1;
995
+ } else if (arg === "--auto-scout-max-daily") {
996
+ parsed.autoScoutMaxDaily = Number(next) || 0;
997
+ index += 1;
685
998
  } else if (arg === "--help" || arg === "-h") {
686
999
  parsed.command = "help";
687
1000
  } else {
package/web/app.css CHANGED
@@ -1932,7 +1932,7 @@ code {
1932
1932
 
1933
1933
  .detail-page-copy--approval {
1934
1934
  margin: 0.78rem 0 0.48rem;
1935
- padding: 0.06rem 0;
1935
+ padding: 0.25rem 0.4rem;
1936
1936
  display: grid;
1937
1937
  gap: 0;
1938
1938
  line-height: 1.24;
@@ -2259,6 +2259,7 @@ code {
2259
2259
 
2260
2260
  .reply-composer__description {
2261
2261
  margin: 0;
2262
+ padding: 0.75rem 1rem;
2262
2263
  }
2263
2264
 
2264
2265
  .reply-composer__form {
@@ -2266,6 +2267,94 @@ code {
2266
2267
  gap: 0.8rem;
2267
2268
  }
2268
2269
 
2270
+ .reply-composer__context {
2271
+ margin: 0.4rem 0;
2272
+ padding: 0.6rem 0.75rem;
2273
+ border-radius: 12px;
2274
+ background: rgba(255, 255, 255, 0.03);
2275
+ border: 1px solid var(--line);
2276
+ font-size: 0.9rem;
2277
+ }
2278
+ .reply-composer__context summary {
2279
+ cursor: pointer;
2280
+ color: var(--muted);
2281
+ }
2282
+ .reply-composer__context-body {
2283
+ margin-top: 0.5rem;
2284
+ line-height: 1.55;
2285
+ color: var(--text);
2286
+ }
2287
+ .reply-composer__intent {
2288
+ margin: 0.5rem 0;
2289
+ padding: 0.6rem 0.75rem;
2290
+ border-radius: 12px;
2291
+ background: rgba(110, 168, 255, 0.08);
2292
+ border: 1px solid rgba(110, 168, 255, 0.25);
2293
+ }
2294
+ .reply-composer__intent p {
2295
+ margin: 0.35rem 0 0;
2296
+ line-height: 1.5;
2297
+ }
2298
+ .reply-composer__author-meta {
2299
+ margin: 0.1rem 0 0.4rem;
2300
+ font-size: 0.85rem;
2301
+ }
2302
+ .reply-composer__textarea {
2303
+ width: 100%;
2304
+ min-height: 14rem;
2305
+ padding: 0.85rem 0.95rem;
2306
+ border-radius: 14px;
2307
+ border: 1px solid var(--line);
2308
+ background: rgba(7, 12, 16, 0.96);
2309
+ color: var(--text);
2310
+ font: inherit;
2311
+ font-size: 1rem;
2312
+ line-height: 1.55;
2313
+ resize: vertical;
2314
+ }
2315
+
2316
+ .reply-composer__textarea:focus {
2317
+ outline: none;
2318
+ border-color: var(--accent, #6ea8ff);
2319
+ }
2320
+
2321
+ .compose-greeting {
2322
+ display: flex;
2323
+ align-items: center;
2324
+ gap: 0.55rem;
2325
+ margin: 0.6rem 0 0.4rem;
2326
+ padding: 0.65rem 0.85rem;
2327
+ border-radius: 12px;
2328
+ border-left: 3px solid var(--completion, #d2aa63);
2329
+ background: rgba(210, 170, 99, 0.10);
2330
+ }
2331
+ .compose-greeting__icon {
2332
+ font-size: 1.25rem;
2333
+ line-height: 1;
2334
+ }
2335
+ .compose-greeting__text {
2336
+ font-size: 0.92rem;
2337
+ color: var(--text);
2338
+ font-weight: 500;
2339
+ }
2340
+ .reply-field--title {
2341
+ margin-bottom: 0.5rem;
2342
+ }
2343
+ .reply-field--title .reply-field__input {
2344
+ min-height: auto;
2345
+ padding: 0.65rem 0.85rem;
2346
+ border: 1px solid var(--line);
2347
+ border-radius: 12px;
2348
+ background: rgba(7, 12, 16, 0.96);
2349
+ font-size: 1.05rem;
2350
+ font-weight: 700;
2351
+ }
2352
+
2353
+ .reply-composer__title-display {
2354
+ margin: 0.25rem 0 0.5rem;
2355
+ font-size: 1.05rem;
2356
+ }
2357
+
2269
2358
  .reply-field {
2270
2359
  gap: 0.5rem;
2271
2360
  }
@@ -3347,6 +3436,14 @@ button[data-action-url][aria-busy="true"]:not(.is-loading) {
3347
3436
  color: #f5a45a;
3348
3437
  background: rgba(245, 164, 90, 0.1);
3349
3438
  }
3439
+ .provider-badge--moltbook {
3440
+ color: #e07a5f;
3441
+ background: rgba(224, 122, 95, 0.12);
3442
+ }
3443
+ .provider-badge--a2a {
3444
+ color: #81c784;
3445
+ background: rgba(129, 199, 132, 0.12);
3446
+ }
3350
3447
 
3351
3448
  .provider-filter {
3352
3449
  display: inline-flex;
@@ -3357,18 +3454,22 @@ button[data-action-url][aria-busy="true"]:not(.is-loading) {
3357
3454
  background: rgba(255, 255, 255, 0.04);
3358
3455
  border: 1px solid rgba(255, 255, 255, 0.08);
3359
3456
  border-radius: 999px;
3360
- width: fit-content;
3457
+ max-width: 100%;
3458
+ overflow-x: auto;
3459
+ -webkit-overflow-scrolling: touch;
3361
3460
  }
3362
3461
  .provider-filter__button {
3363
3462
  appearance: none;
3364
3463
  background: transparent;
3365
3464
  color: var(--text-muted, #9aa6b2);
3366
3465
  border: 0;
3367
- padding: 0.3rem 0.85rem;
3466
+ padding: 0.3rem 0.65rem;
3368
3467
  font-size: 0.78rem;
3369
3468
  font-weight: 600;
3370
3469
  border-radius: 999px;
3371
3470
  cursor: pointer;
3471
+ white-space: nowrap;
3472
+ flex-shrink: 0;
3372
3473
  transition: background 0.15s ease, color 0.15s ease;
3373
3474
  }
3374
3475
  .provider-filter__button:hover {