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.
- package/README.md +55 -0
- package/package.json +16 -2
- package/scripts/a2a-cli.mjs +189 -0
- package/scripts/a2a-executor.mjs +126 -0
- package/scripts/a2a-handler.mjs +439 -0
- package/scripts/a2a-relay-client.mjs +394 -0
- package/scripts/com.viveworker.moltbook-scout.plist.sample +58 -0
- package/scripts/moltbook-api.mjs +206 -0
- package/scripts/moltbook-cli.mjs +1372 -0
- package/scripts/moltbook-scout-auto.sh +447 -0
- package/scripts/moltbook-scout-run.sh +14 -0
- package/scripts/moltbook-watcher.mjs +294 -0
- package/scripts/viveworker-bridge.mjs +814 -16
- package/scripts/viveworker-claude-hook.mjs +7 -1
- package/scripts/viveworker.mjs +314 -1
- package/web/app.css +104 -3
- package/web/app.js +445 -28
- package/web/i18n.js +90 -0
- package/web/sw.js +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Automated Moltbook scout with batch scoring window + daily compose.
|
|
3
|
+
#
|
|
4
|
+
# Each 2-min cycle:
|
|
5
|
+
# Step 0: Check if we should compose an original post today (once/day, needs >= 3 activities).
|
|
6
|
+
# Step 1: Check if the batch window has expired. If yes → pick best → draft → propose → exit.
|
|
7
|
+
# Step 2: Find a candidate post; skip if it matches avoid list.
|
|
8
|
+
# Step 3: Score the candidate (0-100) against persona, add to batch.
|
|
9
|
+
#
|
|
10
|
+
# Environment:
|
|
11
|
+
# SCOUT_HARNESS — "claude" or "codex" (default: auto-detect)
|
|
12
|
+
# SCOUT_FLAGS — extra flags for scout (e.g. "--submolts builds,general --max-daily 5")
|
|
13
|
+
# SCOUT_TIMEOUT — propose timeout in seconds (default: 900)
|
|
14
|
+
# SCOUT_WINDOW — batch window in seconds (default: 1800 = 30 min)
|
|
15
|
+
# COMPOSE_MAX — max original posts per day (default: 1)
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
# Portable timeout wrapper (macOS lacks GNU timeout)
|
|
19
|
+
portable_timeout() {
|
|
20
|
+
local secs="$1"; shift
|
|
21
|
+
perl -e 'alarm shift; exec @ARGV' "$secs" "$@"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
NODE="$(command -v node)"
|
|
26
|
+
VIVEWORKER="$SCRIPT_DIR/viveworker.mjs"
|
|
27
|
+
PERSONA_FILE="$HOME/.viveworker/moltbook-persona.md"
|
|
28
|
+
WINDOW_SEC="${SCOUT_WINDOW:-1800}"
|
|
29
|
+
|
|
30
|
+
# Seconds until a given hour (local time). Used for compose slot timeouts.
|
|
31
|
+
seconds_until_hour() {
|
|
32
|
+
local target_hour="$1"
|
|
33
|
+
"$NODE" -e "const n=new Date(),t=new Date(n);t.setHours($target_hour,0,0,0);if(t<=n)t.setDate(t.getDate()+1);console.log(Math.floor((t-n)/1000))"
|
|
34
|
+
}
|
|
35
|
+
WINDOW_MS=$((WINDOW_SEC * 1000))
|
|
36
|
+
MAX_SKIP=5
|
|
37
|
+
|
|
38
|
+
# ── Detect harness ────────────────────────────────────────────
|
|
39
|
+
HARNESS="${SCOUT_HARNESS:-}"
|
|
40
|
+
HARNESS_BIN=""
|
|
41
|
+
if [ -z "$HARNESS" ]; then
|
|
42
|
+
if command -v claude >/dev/null 2>&1; then
|
|
43
|
+
HARNESS="claude"
|
|
44
|
+
elif command -v codex >/dev/null 2>&1; then
|
|
45
|
+
HARNESS="codex"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
if [ -n "$HARNESS" ]; then
|
|
49
|
+
HARNESS_BIN=$(command -v "$HARNESS" 2>/dev/null || true)
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ── Load persona & avoid keywords ─────────────────────────────
|
|
53
|
+
PERSONA=""
|
|
54
|
+
AVOID_PATTERN=""
|
|
55
|
+
if [ -f "$PERSONA_FILE" ]; then
|
|
56
|
+
PERSONA=$(cat "$PERSONA_FILE")
|
|
57
|
+
AVOID_PATTERN=$(echo "$PERSONA" | sed -n '/^## avoid/,/^## /{ /^## /d; p; }' \
|
|
58
|
+
| sed 's/^- *//' \
|
|
59
|
+
| sed '/^$/d' \
|
|
60
|
+
| "$NODE" -e "
|
|
61
|
+
let d=''; process.stdin.on('data',c=>d+=c);
|
|
62
|
+
process.stdin.on('end',()=>{
|
|
63
|
+
const lines=d.trim().split('\n').filter(Boolean);
|
|
64
|
+
const kws=[];
|
|
65
|
+
for(const line of lines){
|
|
66
|
+
for(const part of line.split(/[,\/]/)){
|
|
67
|
+
const w=part.trim().toLowerCase()
|
|
68
|
+
.replace(/[''\"]/g,'')
|
|
69
|
+
.replace(/\s+tone$/,'')
|
|
70
|
+
.replace(/\s+threads?$/,'');
|
|
71
|
+
if(w && w.length>2) kws.push(w);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if(kws.length) console.log(kws.join('|'));
|
|
75
|
+
});
|
|
76
|
+
" 2>/dev/null)
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
[ -n "$AVOID_PATTERN" ] && echo "[scout-auto] avoid pattern: $AVOID_PATTERN"
|
|
80
|
+
|
|
81
|
+
candidate_should_skip() {
|
|
82
|
+
[ -z "$AVOID_PATTERN" ] && return 1
|
|
83
|
+
printf '%s\n%s' "$1" "$2" | grep -iqE "$AVOID_PATTERN"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Temp file for JSON interchange
|
|
87
|
+
SCOUT_TMP=$(mktemp /tmp/viveworker-scout-XXXXXX.json)
|
|
88
|
+
trap 'rm -f "$SCOUT_TMP"' EXIT
|
|
89
|
+
|
|
90
|
+
read_field() {
|
|
91
|
+
"$NODE" -e "
|
|
92
|
+
const fs=require('fs');
|
|
93
|
+
try{const o=JSON.parse(fs.readFileSync('$SCOUT_TMP','utf8'));console.log(o['$1']||'')}catch{}
|
|
94
|
+
"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
json_field() {
|
|
98
|
+
echo "$1" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).$2||'')}catch{}})"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ═══════════════════════════════════════════════════════════════
|
|
102
|
+
# Step 0: Compose an original post (slot-based: morning/noon/evening)
|
|
103
|
+
# - morning (9-12): yesterday's work if not posted yesterday
|
|
104
|
+
# - noon (12-17): morning's work
|
|
105
|
+
# - evening(17+): full day's work
|
|
106
|
+
# Max 3 approved posts/day. Each slot proposed at most once.
|
|
107
|
+
# ═══════════════════════════════════════════════════════════════
|
|
108
|
+
COMPOSE_MAX="${COMPOSE_MAX:-3}"
|
|
109
|
+
COMPOSE_JSON=$("$NODE" "$VIVEWORKER" moltbook compose --max-daily "$COMPOSE_MAX" 2>/dev/null) || true
|
|
110
|
+
COMPOSE_STATUS=$(json_field "$COMPOSE_JSON" "status")
|
|
111
|
+
|
|
112
|
+
if [ "$COMPOSE_STATUS" = "material" ] && [ -n "$HARNESS_BIN" ]; then
|
|
113
|
+
COMPOSE_SLOT=$(json_field "$COMPOSE_JSON" "slot")
|
|
114
|
+
ACTIVITY_COUNT=$(json_field "$COMPOSE_JSON" "activityCount")
|
|
115
|
+
ACTIVITY_SUMMARY=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);const e=o.activitySummary||[];console.log(e.map(x=>'- ['+x.kind+'] '+x.title+(x.summary?' — '+x.summary:'')).join('\n'))}catch{}})")
|
|
116
|
+
RECENT_TITLES=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);console.log((o.recentTitles||[]).join(', '))}catch{}})")
|
|
117
|
+
|
|
118
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): ${ACTIVITY_COUNT} activities found"
|
|
119
|
+
|
|
120
|
+
# Slot-specific tone instructions.
|
|
121
|
+
case "$COMPOSE_SLOT" in
|
|
122
|
+
morning)
|
|
123
|
+
SLOT_TONE="This is a morning update about yesterday's work. Frame it as a recap: what you accomplished, what you learned, any open questions left over."
|
|
124
|
+
;;
|
|
125
|
+
noon)
|
|
126
|
+
SLOT_TONE="This is a midday progress update. Share what you've been working on this morning — focus on the most interesting or challenging part so far."
|
|
127
|
+
;;
|
|
128
|
+
evening)
|
|
129
|
+
SLOT_TONE="This is an end-of-day report. Summarize the full day's work, highlight the key achievement or insight, and mention what's next."
|
|
130
|
+
;;
|
|
131
|
+
*)
|
|
132
|
+
SLOT_TONE="Share what you've been working on recently."
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
|
|
136
|
+
COMPOSE_PROMPT="$(cat <<COMPOSE_EOF
|
|
137
|
+
${PERSONA:+You are composing an original post on Moltbook (an AI agent social network) on behalf of the agent described below.
|
|
138
|
+
|
|
139
|
+
CRITICAL RULES:
|
|
140
|
+
1. Follow this persona EXACTLY — use the same voice, tone, and interests described below.
|
|
141
|
+
2. Only write about activities that overlap with the persona expertise ("i can talk substantively about"). Skip unrelated work entirely.
|
|
142
|
+
3. Respect the "avoid" list — do not mention any avoided topics even if they appear in the activity log.
|
|
143
|
+
4. The post must sound like the persona wrote it naturally, not like a generated summary.
|
|
144
|
+
|
|
145
|
+
-----8<----- persona -----8<-----
|
|
146
|
+
$PERSONA
|
|
147
|
+
-----8<----- end persona -----8<-----
|
|
148
|
+
|
|
149
|
+
}$SLOT_TONE
|
|
150
|
+
|
|
151
|
+
Work activity:
|
|
152
|
+
$ACTIVITY_SUMMARY
|
|
153
|
+
|
|
154
|
+
${RECENT_TITLES:+Recent post titles (avoid repeating similar topics): $RECENT_TITLES
|
|
155
|
+
|
|
156
|
+
}Available submolts: general, builds, tooling, agents, infrastructure
|
|
157
|
+
|
|
158
|
+
Instructions:
|
|
159
|
+
- Filter the activity list through the persona lens — only include work that the persona would genuinely find interesting or worth sharing.
|
|
160
|
+
- If none of the activities match the persona expertise, output only: NO_MATCH
|
|
161
|
+
- Write in the persona authentic voice. Do not summarize mechanically.
|
|
162
|
+
- 2-4 paragraphs. End with a question or open thought to invite discussion.
|
|
163
|
+
|
|
164
|
+
Output format (no markdown fences, no extra text):
|
|
165
|
+
|
|
166
|
+
SUBMOLT: (one of the available submolts)
|
|
167
|
+
TITLE: (concise title, max 300 chars)
|
|
168
|
+
INTENT: (1-2 sentences in Japanese: why this is worth posting from this persona perspective)
|
|
169
|
+
---
|
|
170
|
+
(post body in persona voice)
|
|
171
|
+
COMPOSE_EOF
|
|
172
|
+
)"
|
|
173
|
+
|
|
174
|
+
COMPOSE_DRAFT=""
|
|
175
|
+
if [ "$HARNESS" = "claude" ]; then
|
|
176
|
+
COMPOSE_DRAFT=$(portable_timeout 60 "$HARNESS_BIN" -p "$COMPOSE_PROMPT" --output-format text 2>/dev/null) || true
|
|
177
|
+
elif [ "$HARNESS" = "codex" ]; then
|
|
178
|
+
COMPOSE_DRAFT=$(portable_timeout 60 "$HARNESS_BIN" exec "$COMPOSE_PROMPT" 2>/dev/null) || true
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
if [ -n "$COMPOSE_DRAFT" ]; then
|
|
182
|
+
# Check for NO_MATCH signal (persona doesn't align with today's work).
|
|
183
|
+
if echo "$COMPOSE_DRAFT" | grep -q "NO_MATCH"; then
|
|
184
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): persona has no match with current activities — skipping"
|
|
185
|
+
else
|
|
186
|
+
# Parse SUBMOLT, TITLE, INTENT, and body.
|
|
187
|
+
C_SUBMOLT=$(echo "$COMPOSE_DRAFT" | sed -n 's/^SUBMOLT: *//p' | head -1)
|
|
188
|
+
C_TITLE=$(echo "$COMPOSE_DRAFT" | sed -n 's/^TITLE: *//p' | head -1)
|
|
189
|
+
C_INTENT=""
|
|
190
|
+
C_BODY=""
|
|
191
|
+
if echo "$COMPOSE_DRAFT" | grep -q "^---$"; then
|
|
192
|
+
C_INTENT=$(echo "$COMPOSE_DRAFT" | sed -n 's/^INTENT: *//p' | head -1)
|
|
193
|
+
C_BODY=$(echo "$COMPOSE_DRAFT" | sed '1,/^---$/d')
|
|
194
|
+
else
|
|
195
|
+
C_BODY=$(echo "$COMPOSE_DRAFT" | sed '/^SUBMOLT:/d;/^TITLE:/d;/^INTENT:/d')
|
|
196
|
+
fi
|
|
197
|
+
C_SUBMOLT=${C_SUBMOLT:-general}
|
|
198
|
+
C_TITLE=$(echo "$C_TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
199
|
+
C_INTENT=$(echo "$C_INTENT" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
200
|
+
C_BODY=$(echo "$C_BODY" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
201
|
+
|
|
202
|
+
if [ -n "$C_TITLE" ] && [ -n "$C_BODY" ]; then
|
|
203
|
+
# Timeout = seconds until current slot ends.
|
|
204
|
+
case "$COMPOSE_SLOT" in
|
|
205
|
+
morning) COMPOSE_TIMEOUT=$(seconds_until_hour 12) ;;
|
|
206
|
+
noon) COMPOSE_TIMEOUT=$(seconds_until_hour 17) ;;
|
|
207
|
+
evening) COMPOSE_TIMEOUT=$(seconds_until_hour 24) ;;
|
|
208
|
+
*) COMPOSE_TIMEOUT=1800 ;;
|
|
209
|
+
esac
|
|
210
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): title='$C_TITLE' submolt=$C_SUBMOLT (${#C_BODY} chars, timeout=${COMPOSE_TIMEOUT}s)"
|
|
211
|
+
|
|
212
|
+
"$NODE" "$VIVEWORKER" moltbook compose-propose \
|
|
213
|
+
--title "$C_TITLE" \
|
|
214
|
+
--content "$C_BODY" \
|
|
215
|
+
--submolt "$C_SUBMOLT" \
|
|
216
|
+
--intent "${C_INTENT:-auto-compose: $COMPOSE_SLOT update}" \
|
|
217
|
+
--slot "$COMPOSE_SLOT" \
|
|
218
|
+
--timeout "$COMPOSE_TIMEOUT"
|
|
219
|
+
|
|
220
|
+
exit 0
|
|
221
|
+
else
|
|
222
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): draft parsing failed (title='$C_TITLE', body=${#C_BODY} chars)"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
else
|
|
226
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): harness returned empty draft"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# ═══════════════════════════════════════════════════════════════
|
|
231
|
+
# Step 1: Check if the batch window has expired → pick & draft
|
|
232
|
+
# ═══════════════════════════════════════════════════════════════
|
|
233
|
+
PICK_JSON=$("$NODE" "$VIVEWORKER" moltbook batch-pick --window-ms "$WINDOW_MS" 2>/dev/null) || true
|
|
234
|
+
PICK_STATUS=$(json_field "$PICK_JSON" "status")
|
|
235
|
+
|
|
236
|
+
if [ "$PICK_STATUS" = "picked" ]; then
|
|
237
|
+
BEST_POST_ID=$(json_field "$PICK_JSON" "postId")
|
|
238
|
+
BEST_TITLE=$(json_field "$PICK_JSON" "title")
|
|
239
|
+
BEST_AUTHOR=$(json_field "$PICK_JSON" "author")
|
|
240
|
+
BEST_SCORE=$(json_field "$PICK_JSON" "score")
|
|
241
|
+
CONSIDERED=$(json_field "$PICK_JSON" "consideredCount")
|
|
242
|
+
|
|
243
|
+
echo "[scout-auto] batch complete: picked '$BEST_TITLE' by @$BEST_AUTHOR (score=$BEST_SCORE, considered=$CONSIDERED)"
|
|
244
|
+
|
|
245
|
+
# Fetch post content from Moltbook API
|
|
246
|
+
POST_DATA=$("$NODE" --input-type=module -e "
|
|
247
|
+
import { readFileSync } from 'fs';
|
|
248
|
+
const env = readFileSync(process.env.HOME + '/.viveworker/moltbook.env', 'utf8');
|
|
249
|
+
for(const line of env.split('\n')){
|
|
250
|
+
const m = line.match(/^(\w+)=(.*)\$/);
|
|
251
|
+
if(m) process.env[m[1]] = m[2];
|
|
252
|
+
}
|
|
253
|
+
const { createMoltbookClient } = await import('./scripts/moltbook-api.mjs');
|
|
254
|
+
const { client: mb } = await createMoltbookClient();
|
|
255
|
+
try {
|
|
256
|
+
const post = await mb('/posts/${BEST_POST_ID}');
|
|
257
|
+
const p = post?.post || post;
|
|
258
|
+
console.log(JSON.stringify({ content: p?.content || p?.body || '' }));
|
|
259
|
+
} catch(e) {
|
|
260
|
+
console.log(JSON.stringify({ content: '' }));
|
|
261
|
+
}
|
|
262
|
+
" 2>/dev/null) || echo '{"content":""}'
|
|
263
|
+
POST_CONTENT=$(json_field "$POST_DATA" "content")
|
|
264
|
+
POST_URL="https://www.moltbook.com/post/$BEST_POST_ID"
|
|
265
|
+
|
|
266
|
+
# ── Draft via LLM ────────────────────────────────────────
|
|
267
|
+
if [ -z "$HARNESS_BIN" ]; then
|
|
268
|
+
echo "[scout-auto] error: neither 'claude' nor 'codex' CLI found"
|
|
269
|
+
exit 1
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
DRAFT_PROMPT="$(cat <<PROMPT_EOF
|
|
273
|
+
${PERSONA:+You are drafting a reply on behalf of the agent described below. Follow this persona precisely.
|
|
274
|
+
-----8<----- persona -----8<-----
|
|
275
|
+
$PERSONA
|
|
276
|
+
-----8<----- end persona -----8<-----
|
|
277
|
+
|
|
278
|
+
}You are replying to the following post on Moltbook (an AI agent social network).
|
|
279
|
+
|
|
280
|
+
Title: $BEST_TITLE
|
|
281
|
+
Author: @$BEST_AUTHOR
|
|
282
|
+
URL: $POST_URL
|
|
283
|
+
|
|
284
|
+
Post body:
|
|
285
|
+
$POST_CONTENT
|
|
286
|
+
|
|
287
|
+
Draft a reply in the persona voice (or, if no persona is set, informal lowercase, 2-3 paragraphs, technically substantive, no signature). Prefer ending on one concrete question or a conceded open problem.
|
|
288
|
+
|
|
289
|
+
Output in this exact format — no markdown fences, no extra text:
|
|
290
|
+
|
|
291
|
+
INTENT: (1-2 sentences in Japanese: why this post caught your attention and what angle your reply takes — this is shown to the human operator for approval context)
|
|
292
|
+
---
|
|
293
|
+
(your reply text here)
|
|
294
|
+
PROMPT_EOF
|
|
295
|
+
)"
|
|
296
|
+
|
|
297
|
+
echo "[scout-auto] drafting via $HARNESS for '$BEST_TITLE'"
|
|
298
|
+
DRAFT_TEXT=""
|
|
299
|
+
if [ "$HARNESS" = "claude" ]; then
|
|
300
|
+
DRAFT_TEXT=$("$HARNESS_BIN" -p "$DRAFT_PROMPT" --output-format text) || true
|
|
301
|
+
elif [ "$HARNESS" = "codex" ]; then
|
|
302
|
+
DRAFT_TEXT=$("$HARNESS_BIN" exec "$DRAFT_PROMPT") || true
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
if [ -z "$DRAFT_TEXT" ]; then
|
|
306
|
+
echo "[scout-auto] error: harness returned empty draft"
|
|
307
|
+
exit 1
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# Parse INTENT and reply body
|
|
311
|
+
INTENT=""
|
|
312
|
+
REPLY_BODY=""
|
|
313
|
+
if echo "$DRAFT_TEXT" | grep -q "^---$"; then
|
|
314
|
+
INTENT=$(echo "$DRAFT_TEXT" | sed -n '1,/^---$/p' | sed '/^---$/d' | sed 's/^INTENT: *//')
|
|
315
|
+
REPLY_BODY=$(echo "$DRAFT_TEXT" | sed '1,/^---$/d')
|
|
316
|
+
else
|
|
317
|
+
REPLY_BODY="$DRAFT_TEXT"
|
|
318
|
+
INTENT="auto-scout: responding to @${BEST_AUTHOR}'s post (score=$BEST_SCORE)"
|
|
319
|
+
fi
|
|
320
|
+
INTENT=$(echo "$INTENT" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
321
|
+
REPLY_BODY=$(echo "$REPLY_BODY" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
322
|
+
|
|
323
|
+
if [ -z "$REPLY_BODY" ]; then
|
|
324
|
+
echo "[scout-auto] error: parsed draft body is empty"
|
|
325
|
+
exit 1
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
echo "[scout-auto] intent: $INTENT"
|
|
329
|
+
echo "[scout-auto] draft ready (${#REPLY_BODY} chars), submitting propose"
|
|
330
|
+
|
|
331
|
+
# ── Propose ──────────────────────────────────────────────
|
|
332
|
+
"$NODE" "$VIVEWORKER" moltbook propose "$BEST_POST_ID" \
|
|
333
|
+
--title "$BEST_TITLE" \
|
|
334
|
+
--post-author "$BEST_AUTHOR" \
|
|
335
|
+
--post-body "$POST_CONTENT" \
|
|
336
|
+
--intent "$INTENT" \
|
|
337
|
+
--text "$REPLY_BODY" \
|
|
338
|
+
--timeout "$WINDOW_SEC"
|
|
339
|
+
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# ═══════════════════════════════════════════════════════════════
|
|
344
|
+
# Step 2 & 3: Find a candidate, score, add to batch
|
|
345
|
+
# ═══════════════════════════════════════════════════════════════
|
|
346
|
+
echo "[scout-auto] collecting candidates (window: ${WINDOW_SEC}s)"
|
|
347
|
+
SKIP_COUNT=0
|
|
348
|
+
|
|
349
|
+
while [ $SKIP_COUNT -lt $MAX_SKIP ]; do
|
|
350
|
+
"$NODE" "$VIVEWORKER" moltbook scout ${SCOUT_FLAGS:-} > "$SCOUT_TMP" 2>/dev/null || true
|
|
351
|
+
|
|
352
|
+
STATUS=$("$NODE" -e "
|
|
353
|
+
const fs=require('fs');
|
|
354
|
+
try{console.log(JSON.parse(fs.readFileSync('$SCOUT_TMP','utf8')).status)}catch{console.log('error')}
|
|
355
|
+
")
|
|
356
|
+
|
|
357
|
+
if [ "$STATUS" = "already-engaged" ]; then
|
|
358
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
359
|
+
echo "[scout-auto] already engaged, retrying [$SKIP_COUNT/$MAX_SKIP]"
|
|
360
|
+
continue
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
if [ "$STATUS" != "candidate" ]; then
|
|
364
|
+
echo "[scout-auto] no candidate (status=$STATUS)"
|
|
365
|
+
exit 0
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
POST_ID=$(read_field postId)
|
|
369
|
+
POST_TITLE=$(read_field title)
|
|
370
|
+
POST_AUTHOR=$(read_field author)
|
|
371
|
+
POST_CONTENT=$(read_field content)
|
|
372
|
+
POST_URL=$(read_field postUrl)
|
|
373
|
+
POST_SUBMOLT=$(read_field submolt)
|
|
374
|
+
|
|
375
|
+
# Keyword filter
|
|
376
|
+
if candidate_should_skip "$POST_TITLE" "$POST_CONTENT"; then
|
|
377
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
378
|
+
echo "[scout-auto] skipping (avoid keyword): $POST_TITLE (by @$POST_AUTHOR) [$SKIP_COUNT/$MAX_SKIP]"
|
|
379
|
+
"$NODE" "$VIVEWORKER" moltbook mark-scout-seen "$POST_ID" 2>/dev/null || true
|
|
380
|
+
continue
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
# LLM scoring (0-100)
|
|
384
|
+
SCORE=0
|
|
385
|
+
if [ -n "$PERSONA" ] && [ -n "$HARNESS_BIN" ]; then
|
|
386
|
+
SCORE_PROMPT="You are scoring Moltbook posts for an AI agent. Rate how well this post matches the agent's expertise and interests on a scale of 0-100.
|
|
387
|
+
|
|
388
|
+
Score 0: post matches the avoid list, or is completely outside the agent's domain.
|
|
389
|
+
Score 1-30: tangentially related but the agent has little to add.
|
|
390
|
+
Score 31-60: relevant topic, the agent could contribute meaningfully.
|
|
391
|
+
Score 61-80: strong match with the agent's core expertise.
|
|
392
|
+
Score 81-100: excellent match — the agent has direct experience and a unique angle.
|
|
393
|
+
|
|
394
|
+
Persona:
|
|
395
|
+
$PERSONA
|
|
396
|
+
|
|
397
|
+
Post title: $POST_TITLE
|
|
398
|
+
Post author: @$POST_AUTHOR
|
|
399
|
+
Post body (first 500 chars): ${POST_CONTENT:0:500}
|
|
400
|
+
|
|
401
|
+
Reply with ONLY a number from 0 to 100. Nothing else."
|
|
402
|
+
|
|
403
|
+
SCORE_RAW=""
|
|
404
|
+
if [ "$HARNESS" = "claude" ]; then
|
|
405
|
+
SCORE_RAW=$(portable_timeout 30 "$HARNESS_BIN" -p "$SCORE_PROMPT" --output-format text 2>/dev/null) || true
|
|
406
|
+
elif [ "$HARNESS" = "codex" ]; then
|
|
407
|
+
SCORE_RAW=$(portable_timeout 30 "$HARNESS_BIN" exec "$SCORE_PROMPT" 2>/dev/null) || true
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
SCORE=$(echo "$SCORE_RAW" | tr -dc '0-9\n' | head -1)
|
|
411
|
+
SCORE=${SCORE:-0}
|
|
412
|
+
|
|
413
|
+
if [ "$SCORE" -eq 0 ] 2>/dev/null; then
|
|
414
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
415
|
+
echo "[scout-auto] skipping (score=0): $POST_TITLE (by @$POST_AUTHOR) [$SKIP_COUNT/$MAX_SKIP]"
|
|
416
|
+
"$NODE" "$VIVEWORKER" moltbook mark-scout-seen "$POST_ID" 2>/dev/null || true
|
|
417
|
+
continue
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
if [ -z "$SCORE_RAW" ]; then
|
|
421
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
422
|
+
echo "[scout-auto] skipping (score failed): $POST_TITLE (by @$POST_AUTHOR) [$SKIP_COUNT/$MAX_SKIP]"
|
|
423
|
+
continue
|
|
424
|
+
fi
|
|
425
|
+
else
|
|
426
|
+
SCORE=50
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
echo "[scout-auto] scored $SCORE: $POST_TITLE (by @$POST_AUTHOR)"
|
|
430
|
+
|
|
431
|
+
# Add to batch
|
|
432
|
+
"$NODE" "$VIVEWORKER" moltbook batch-add "$POST_ID" \
|
|
433
|
+
--score "$SCORE" \
|
|
434
|
+
--title "$POST_TITLE" \
|
|
435
|
+
--author "$POST_AUTHOR" \
|
|
436
|
+
--post-url "$POST_URL" \
|
|
437
|
+
--submolt "$POST_SUBMOLT" \
|
|
438
|
+
--window-ms "$WINDOW_MS" 2>/dev/null || true
|
|
439
|
+
|
|
440
|
+
break
|
|
441
|
+
done
|
|
442
|
+
|
|
443
|
+
if [ $SKIP_COUNT -ge $MAX_SKIP ]; then
|
|
444
|
+
echo "[scout-auto] all $MAX_SKIP candidates skipped this cycle"
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
echo "[scout-auto] cycle done — waiting for next invocation"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Provider-neutral entry point for one Moltbook scouting pass.
|
|
3
|
+
#
|
|
4
|
+
# Outputs a JSON candidate (or status) on stdout. The calling agent
|
|
5
|
+
# (Codex Desktop, Claude Desktop, Claude Code, or a human) is expected
|
|
6
|
+
# to read the candidate, draft a reply, and then run:
|
|
7
|
+
#
|
|
8
|
+
# node scripts/viveworker.mjs moltbook propose <postId> --text "<draft>"
|
|
9
|
+
#
|
|
10
|
+
# Pass any extra flags through, e.g. --max-daily 3 --submolts builds,tooling
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
cd "$SCRIPT_DIR/.."
|
|
14
|
+
exec node scripts/viveworker.mjs moltbook scout "$@"
|