viveworker 0.4.7 → 0.4.8

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.
@@ -31,6 +31,8 @@ import {
31
31
  rollScoutDayIfNeeded,
32
32
  markPostSeen,
33
33
  recordComposeAttempt,
34
+ solveVerificationPuzzle,
35
+ solvePuzzleWithLLM,
34
36
  } from "./moltbook-api.mjs";
35
37
 
36
38
  function fail(message, code = 1) {
@@ -486,7 +488,7 @@ async function cmdPropose(postId, flags) {
486
488
  if (!postId) fail("usage: viveworker moltbook propose <postId> --text \"...\"");
487
489
  const text = typeof flags.text === "string" ? flags.text : "";
488
490
  if (!text.trim()) fail("--text is required and must be non-empty");
489
- const timeoutSec = Number(flags.timeout) || 900;
491
+ const ttlSec = Number(flags.ttl) || Number(flags.timeout) || 86400;
490
492
  const parentCommentId = typeof flags["parent-id"] === "string" ? flags["parent-id"] : "";
491
493
  const postTitle = typeof flags.title === "string" ? flags.title : "";
492
494
  const postBody = typeof flags["post-body"] === "string" ? flags["post-body"] : "";
@@ -504,7 +506,8 @@ async function cmdPropose(postId, flags) {
504
506
 
505
507
  const sourceId = `draft:${postId}:${Date.now()}`;
506
508
 
507
- // 1. Submit draft to bridge.
509
+ // Submit draft to bridge. The bridge persists it to disk and handles posting
510
+ // on approval — the CLI exits immediately (fire-and-forget).
508
511
  let submitRes;
509
512
  try {
510
513
  const r = await fetch(`${base}/api/providers/moltbook/draft`, {
@@ -520,6 +523,7 @@ async function cmdPropose(postId, flags) {
520
523
  parentCommentId,
521
524
  intent,
522
525
  draftText: text,
526
+ ttlMs: ttlSec * 1000,
523
527
  contextSummary: truncate(intent || text, 160),
524
528
  }),
525
529
  });
@@ -534,303 +538,10 @@ async function cmdPropose(postId, flags) {
534
538
  const token = submitRes?.token;
535
539
  if (!token) fail(`bridge did not return a token: ${JSON.stringify(submitRes)}`);
536
540
 
537
- console.log(`propose: draft submitted (token=${token}); waiting for phone decision (timeout=${timeoutSec}s)`);
538
-
539
- // 2. Long-poll decision.
540
- const deadline = Date.now() + timeoutSec * 1000;
541
- let decision = null;
542
- while (Date.now() < deadline && !decision) {
543
- const remain = Math.min(60, Math.ceil((deadline - Date.now()) / 1000));
544
- try {
545
- const r = await fetch(
546
- `${base}/api/providers/moltbook/draft/${encodeURIComponent(token)}/decision?wait=${remain}`,
547
- {
548
- method: "GET",
549
- headers: { "x-viveworker-hook-secret": secret },
550
- }
551
- );
552
- if (r.ok) {
553
- const body = await r.json();
554
- if (body && body.status === "decided") decision = body;
555
- }
556
- } catch {
557
- // transient — retry
558
- }
559
- }
560
-
561
- if (!decision) {
562
- console.log("propose: timed out waiting for phone decision — treating as deny");
563
- process.exit(2);
564
- }
565
-
566
- if (decision.action === "deny") {
567
- console.log(`propose: denied by phone${decision.reason ? ` (${decision.reason})` : ""}`);
568
- process.exit(1);
569
- }
570
-
571
- // 3. Approve path: use the (possibly edited) text to actually post.
572
- const finalText = decision.text || text;
573
- const { mb } = await getClient();
574
- const result = await mb(`/posts/${postId}/comments`, {
575
- method: "POST",
576
- body: JSON.stringify({
577
- content: finalText,
578
- ...(parentCommentId ? { parent_id: parentCommentId } : {}),
579
- }),
580
- });
581
- const comment = result?.comment || null;
582
- const verification = comment?.verification || null;
583
-
584
- console.log(
585
- JSON.stringify(
586
- { ok: true, postId, action: "posted", commentId: comment?.id, verification },
587
- null,
588
- 2
589
- )
590
- );
591
-
592
- // Bump sentToday counter immediately after posting (before verification),
593
- // because the comment is already created and rate-limits are consumed
594
- // regardless of verification outcome.
595
- const state = rollScoutDayIfNeeded(await readScoutState());
596
- state.sentToday += 1;
597
- markPostSeen(state, postId, "published");
598
- await writeScoutState(state);
599
-
600
- if (!verification) {
601
- console.log("propose: no verification challenge returned — done");
602
- return;
603
- }
604
-
605
- // 4. Solve verification puzzle inline (try naive solver, then LLM fallback, retry once on wrong answer).
606
- let answer = solveVerificationPuzzle(verification.challenge_text);
607
- const source = answer != null ? "solver" : "skip";
608
- console.log(`propose: verification puzzle — solver answer: ${answer ?? "(null)"}`);
609
-
610
- // If solver couldn't parse, try LLM fallback.
611
- if (answer == null) {
612
- answer = await solvePuzzleWithLLM(verification.challenge_text);
613
- if (answer) console.log(`propose: LLM fallback answer: ${answer}`);
614
- }
615
-
616
- if (answer == null) {
617
- console.log("");
618
- console.log("VERIFICATION REQUIRED (solver + LLM both failed):");
619
- console.log(` verification_code: ${verification.verification_code}`);
620
- console.log(` challenge_text: ${verification.challenge_text}`);
621
- console.log("");
622
- console.log("Solve manually and run:");
623
- console.log(` viveworker moltbook verify ${verification.verification_code} <answer>`);
624
- return;
625
- }
626
-
627
- let verifyRes;
628
- try {
629
- verifyRes = await mb(`/verify`, {
630
- method: "POST",
631
- body: JSON.stringify({ verification_code: verification.verification_code, answer }),
632
- });
633
- } catch (verifyError) {
634
- // Wrong answer — retry with LLM if the first attempt was from solver.
635
- const isWrongAnswer = /incorrect/i.test(verifyError.message);
636
- if (isWrongAnswer && source === "solver") {
637
- console.log(`propose: solver answer ${answer} was wrong, trying LLM fallback`);
638
- const llmAnswer = await solvePuzzleWithLLM(verification.challenge_text);
639
- if (llmAnswer && llmAnswer !== answer) {
640
- console.log(`propose: LLM retry answer: ${llmAnswer}`);
641
- try {
642
- verifyRes = await mb(`/verify`, {
643
- method: "POST",
644
- body: JSON.stringify({ verification_code: verification.verification_code, answer: llmAnswer }),
645
- });
646
- answer = llmAnswer;
647
- } catch (retryError) {
648
- console.log(`propose: LLM retry also failed: ${retryError.message}`);
649
- return;
650
- }
651
- } else {
652
- console.log(`propose: LLM couldn't produce a different answer`);
653
- return;
654
- }
655
- } else {
656
- console.log(`propose: verify failed: ${verifyError.message}`);
657
- return;
658
- }
659
- }
660
- console.log(JSON.stringify({ ok: true, verify: verifyRes, answer }, null, 2));
661
-
541
+ console.log(JSON.stringify({ ok: true, token, ttlSec, fireAndForget: true }));
662
542
  if (!prevTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTls ?? "";
663
543
  }
664
544
 
665
- // LLM-based verification puzzle solver. Shells out to claude or codex CLI.
666
- // Returns the answer as "XX.XX" string, or null if unavailable.
667
- async function solvePuzzleWithLLM(challengeText) {
668
- if (!challengeText) return null;
669
- const prompt =
670
- `The following text is an obfuscated arithmetic word problem from Moltbook (an AI social network). ` +
671
- `The text has random capitalization, doubled letters, and stray punctuation — ignore all of that. ` +
672
- `CRITICAL: ALL symbols (/, *, ^, ~, [, ], etc.) are NOISE, NOT arithmetic operators. ` +
673
- `The operation is ALWAYS expressed in natural language words only. ` +
674
- `Extract the numbers (written as words like "thirty five" = 35), determine the operation from WORDS ONLY ` +
675
- `(addition: "total", "combined", "and", "plus", "gains", "new velocity"; ` +
676
- `subtraction: "difference", "minus", "less", "loses"; ` +
677
- `multiplication: "times", "product", "multiplied"; ` +
678
- `division: "divided by", "ratio", "per"). ` +
679
- `If no operation word is found, default to addition. ` +
680
- `Compute the result and output ONLY the number with exactly 2 decimal places (e.g. "58.00"). No other text.\n\n` +
681
- `Puzzle: ${challengeText}`;
682
-
683
- // Try claude first, then codex.
684
- for (const cmd of ["claude", "codex"]) {
685
- let bin;
686
- try {
687
- bin = await new Promise((resolve) => {
688
- const p = spawn("command", ["-v", cmd], { shell: "/bin/bash", stdio: ["ignore", "pipe", "ignore"] });
689
- let out = "";
690
- p.stdout.on("data", (d) => (out += d.toString()));
691
- p.on("exit", (code) => resolve(code === 0 ? out.trim() : ""));
692
- p.on("error", () => resolve(""));
693
- });
694
- } catch { bin = ""; }
695
- if (!bin) continue;
696
-
697
- const args = cmd === "claude" ? ["-p", prompt, "--output-format", "text"] : ["exec", prompt];
698
- try {
699
- const result = await new Promise((resolve, reject) => {
700
- const p = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 30000 });
701
- let out = "";
702
- p.stdout.on("data", (d) => (out += d.toString()));
703
- p.on("exit", (code) => (code === 0 ? resolve(out.trim()) : reject(new Error(`exit ${code}`))));
704
- p.on("error", reject);
705
- });
706
- // Extract the number from output (LLM might add extra text).
707
- const match = result.match(/(\d+\.\d{2})/);
708
- if (match) return match[1];
709
- // Try integer and append .00
710
- const intMatch = result.match(/^(\d+)$/m);
711
- if (intMatch) return `${intMatch[1]}.00`;
712
- } catch {
713
- // try next
714
- }
715
- }
716
- return null;
717
- }
718
-
719
- // Naive verification-puzzle solver. Handles the obfuscated two-number
720
- // arithmetic Moltbook currently uses (add / subtract / multiply). Returns
721
- // `null` if it can't confidently solve — caller falls back to manual.
722
- function solveVerificationPuzzle(challengeText) {
723
- if (!challengeText) return null;
724
- // Strip ALL symbolic characters as noise — operations are expressed in
725
- // natural language only ("and", "times", "divided by", etc.). Previous
726
- // regex missed `*` and `@` which caused `/` or `*` to be mistaken for
727
- // arithmetic operators.
728
- const cleaned = String(challengeText)
729
- .replace(/[^a-zA-Z0-9\s]/g, " ")
730
- .toLowerCase();
731
- const numberWords = {
732
- zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9,
733
- ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16,
734
- seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, thirty: 30, forty: 40, fifty: 50,
735
- sixty: 60, seventy: 70, eighty: 80, ninety: 90, hundred: 100,
736
- };
737
- // Tokenize loosely by collapsing whitespace. Words sometimes have stray
738
- // characters (e.g. "tw eLvE") so strip non-letters between word fragments.
739
- // Moltbook's obfuscator randomly doubles letters ("tWeNnTy" → "twennty"),
740
- // so try matching both the raw word and a version with collapsed runs.
741
- // We can't blindly collapse because natural doubles exist ("three" has "ee").
742
- const collapseRuns = (w) => w.replace(/([a-z])\1+/g, "$1");
743
- const rawTokens = cleaned
744
- .replace(/[^a-z0-9\s]/g, " ")
745
- .split(/\s+/)
746
- .filter(Boolean);
747
-
748
- // The obfuscator inserts spaces mid-word (e.g. "th ree" → "three",
749
- // "to tal" → "total"). Greedily merge adjacent tokens if the combined
750
- // form (raw or collapsed) matches a known number word or operation keyword.
751
- const operationWords = new Set([
752
- "total", "combined", "force", "velocity", "speed", "gains", "plus", "and",
753
- "subtract", "minus", "less", "difference", "decreased", "loses", "lost", "slower", "slows", "slowed",
754
- "multiply", "times", "product", "multiplied",
755
- "divide", "divided", "ratio",
756
- "how", "much", "what", "exerts", "new",
757
- ]);
758
- const isKnown = (w) => numberWords[w] != null || numberWords[collapseRuns(w)] != null || operationWords.has(w) || operationWords.has(collapseRuns(w));
759
- const merged = [];
760
- let ti = 0;
761
- while (ti < rawTokens.length) {
762
- // Try merging up to 4 consecutive tokens.
763
- let best = rawTokens[ti];
764
- let bestLen = 1;
765
- let candidate = rawTokens[ti];
766
- for (let span = 2; span <= Math.min(4, rawTokens.length - ti); span++) {
767
- candidate += rawTokens[ti + span - 1];
768
- if (isKnown(candidate) || isKnown(collapseRuns(candidate))) {
769
- best = candidate;
770
- bestLen = span;
771
- }
772
- }
773
- merged.push(best);
774
- ti += bestLen;
775
- }
776
-
777
- // For each word, prefer the raw form if it's in the dictionary; otherwise try collapsed.
778
- const words = merged.map((w) => {
779
- if (/^\d+$/.test(w)) return w;
780
- if (numberWords[w] != null) return w;
781
- const collapsed = collapseRuns(w);
782
- if (numberWords[collapsed] != null) return collapsed;
783
- return collapsed; // default to collapsed for non-number words (operation keywords etc.)
784
- });
785
-
786
- // Reconstruct compound numbers like "twenty three" → 23, "one hundred
787
- // twenty" → 120.
788
- const numbers = [];
789
- let i = 0;
790
- while (i < words.length) {
791
- const w = words[i];
792
- if (/^\d+$/.test(w)) {
793
- numbers.push(Number(w));
794
- i += 1;
795
- continue;
796
- }
797
- if (numberWords[w] != null) {
798
- let total = numberWords[w];
799
- i += 1;
800
- while (i < words.length && numberWords[words[i]] != null) {
801
- const next = numberWords[words[i]];
802
- if (next === 100) total *= 100;
803
- else if (next < 100 && total < 100) total += next;
804
- else break;
805
- i += 1;
806
- }
807
- numbers.push(total);
808
- continue;
809
- }
810
- i += 1;
811
- }
812
-
813
- if (numbers.length < 2) return null;
814
- const a = numbers[0];
815
- const b = numbers[1];
816
-
817
- const hasWord = (w) => words.includes(w);
818
- const hasAny = (...ws) => ws.some(hasWord);
819
- let result;
820
- if (hasAny("subtract", "minus", "less", "difference", "decreased", "loses", "lost", "slower", "slows", "slowed")) {
821
- result = a - b;
822
- } else if (hasAny("multiply", "times", "product", "multiplied")) {
823
- result = a * b;
824
- } else if (hasAny("divide", "divided", "ratio")) {
825
- result = b !== 0 ? a / b : a;
826
- } else {
827
- // Default to addition — the Moltbook puzzles overwhelmingly ask for
828
- // "total force", "new speed", "combined", "gains", etc.
829
- result = a + b;
830
- }
831
- return result.toFixed(2);
832
- }
833
-
834
545
  async function cmdMarkScoutSeen(postId) {
835
546
  if (!postId) fail("usage: viveworker moltbook mark-scout-seen <postId>");
836
547
  const state = rollScoutDayIfNeeded(await readScoutState());
@@ -1153,7 +864,7 @@ async function cmdComposePropose(flags) {
1153
864
  const submolt = typeof flags.submolt === "string" ? flags.submolt : "general";
1154
865
  const intent = typeof flags.intent === "string" ? flags.intent : "";
1155
866
  const slot = typeof flags.slot === "string" ? flags.slot : "";
1156
- const timeoutSec = Number(flags.timeout) || 900;
867
+ const ttlSec = Number(flags.ttl) || Number(flags.timeout) || 86400;
1157
868
  if (!title.trim()) fail("--title is required");
1158
869
  if (!content.trim()) fail("--content is required");
1159
870
 
@@ -1167,7 +878,8 @@ async function cmdComposePropose(flags) {
1167
878
 
1168
879
  const sourceId = `compose:${Date.now()}`;
1169
880
 
1170
- // 1. Submit draft to bridge.
881
+ // Submit draft to bridge. The bridge persists it to disk and handles posting
882
+ // on approval — the CLI exits immediately (fire-and-forget).
1171
883
  let submitRes;
1172
884
  try {
1173
885
  const r = await fetch(`${base}/api/providers/moltbook/draft`, {
@@ -1182,6 +894,7 @@ async function cmdComposePropose(flags) {
1182
894
  submoltName: submolt,
1183
895
  intent,
1184
896
  slot,
897
+ ttlMs: ttlSec * 1000,
1185
898
  contextSummary: truncate(intent || content, 160),
1186
899
  }),
1187
900
  });
@@ -1196,105 +909,7 @@ async function cmdComposePropose(flags) {
1196
909
  const token = submitRes?.token;
1197
910
  if (!token) fail(`bridge did not return a token: ${JSON.stringify(submitRes)}`);
1198
911
 
1199
- console.log(`compose-propose: draft submitted (token=${token}); waiting for decision (timeout=${timeoutSec}s)`);
1200
-
1201
- // 2. Long-poll decision.
1202
- const deadline = Date.now() + timeoutSec * 1000;
1203
- let decision = null;
1204
- while (Date.now() < deadline && !decision) {
1205
- const remain = Math.min(60, Math.ceil((deadline - Date.now()) / 1000));
1206
- try {
1207
- const r = await fetch(
1208
- `${base}/api/providers/moltbook/draft/${encodeURIComponent(token)}/decision?wait=${remain}`,
1209
- { method: "GET", headers: { "x-viveworker-hook-secret": secret } }
1210
- );
1211
- if (r.ok) {
1212
- const body = await r.json();
1213
- if (body && body.status === "decided") decision = body;
1214
- }
1215
- } catch { /* transient — retry */ }
1216
- }
1217
-
1218
- if (!decision) {
1219
- console.log("compose-propose: timed out — treating as deny");
1220
- process.exit(2);
1221
- }
1222
- if (decision.action === "deny") {
1223
- console.log("compose-propose: denied by phone");
1224
- process.exit(1);
1225
- }
1226
-
1227
- // 3. Approve path: create original post.
1228
- const finalTitle = decision.title || title;
1229
- const finalContent = decision.text || content;
1230
- const { mb } = await getClient();
1231
- const result = await mb(`/posts`, {
1232
- method: "POST",
1233
- body: JSON.stringify({
1234
- submolt_name: submolt,
1235
- submolt,
1236
- title: finalTitle,
1237
- content: finalContent,
1238
- }),
1239
- });
1240
- const post = result?.post || null;
1241
- const verification = post?.verification || null;
1242
-
1243
- console.log(JSON.stringify({ ok: true, action: "posted", postId: post?.id, verification }, null, 2));
1244
-
1245
- if (!verification) {
1246
- console.log("compose-propose: no verification — done");
1247
- } else {
1248
- // Solve verification puzzle inline (try naive solver, then LLM fallback, retry once on wrong answer).
1249
- let answer = solveVerificationPuzzle(verification.challenge_text);
1250
- const source = answer != null ? "solver" : "skip";
1251
- console.log(`compose-propose: verification puzzle — solver answer: ${answer ?? "(null)"}`);
1252
-
1253
- if (answer == null) {
1254
- answer = await solvePuzzleWithLLM(verification.challenge_text);
1255
- if (answer) console.log(`compose-propose: LLM fallback answer: ${answer}`);
1256
- }
1257
-
1258
- if (answer == null) {
1259
- console.log(`VERIFICATION REQUIRED (solver + LLM both failed):\n verification_code: ${verification.verification_code}\n challenge_text: ${verification.challenge_text}`);
1260
- } else {
1261
- try {
1262
- const verifyRes = await mb(`/verify`, {
1263
- method: "POST",
1264
- body: JSON.stringify({ verification_code: verification.verification_code, answer }),
1265
- });
1266
- console.log(JSON.stringify({ ok: true, verify: verifyRes, answer }, null, 2));
1267
- } catch (verifyError) {
1268
- const isWrongAnswer = /incorrect/i.test(verifyError.message);
1269
- if (isWrongAnswer && source === "solver") {
1270
- console.log(`compose-propose: solver answer ${answer} was wrong, trying LLM fallback`);
1271
- const llmAnswer = await solvePuzzleWithLLM(verification.challenge_text);
1272
- if (llmAnswer && llmAnswer !== answer) {
1273
- console.log(`compose-propose: LLM retry answer: ${llmAnswer}`);
1274
- try {
1275
- const verifyRes2 = await mb(`/verify`, {
1276
- method: "POST",
1277
- body: JSON.stringify({ verification_code: verification.verification_code, answer: llmAnswer }),
1278
- });
1279
- console.log(JSON.stringify({ ok: true, verify: verifyRes2, answer: llmAnswer }, null, 2));
1280
- } catch (retryError) {
1281
- console.log(`compose-propose: LLM retry also failed: ${retryError.message}`);
1282
- }
1283
- } else {
1284
- console.log(`compose-propose: LLM couldn't produce a different answer`);
1285
- }
1286
- } else {
1287
- console.log(`compose-propose: verify failed: ${verifyError.message}`);
1288
- }
1289
- }
1290
- }
1291
- }
1292
-
1293
- // 4. Bump compose counter.
1294
- const state = rollScoutDayIfNeeded(await readScoutState());
1295
- recordComposeAttempt(state, finalTitle, post?.id);
1296
- await writeScoutState(state);
1297
-
912
+ console.log(JSON.stringify({ ok: true, token, ttlSec, fireAndForget: true }));
1298
913
  if (!prevTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTls ?? "";
1299
914
  }
1300
915