viveworker 0.4.6 → 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.
- package/package.json +1 -1
- package/scripts/a2a-executor.mjs +157 -19
- package/scripts/a2a-handler.mjs +2 -1
- package/scripts/a2a-relay-client.mjs +9 -0
- package/scripts/moltbook-api.mjs +209 -0
- package/scripts/moltbook-cli.mjs +12 -397
- package/scripts/viveworker-bridge.mjs +917 -47
- package/scripts/viveworker-claude-hook.mjs +74 -1
- package/web/app.css +123 -1
- package/web/app.js +367 -37
- package/web/i18n.js +75 -3
package/scripts/moltbook-cli.mjs
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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
|
|