viveworker 0.2.0 → 0.3.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 +41 -0
- package/package.json +6 -1
- package/scripts/com.viveworker.moltbook-scout.plist.sample +58 -0
- package/scripts/moltbook-api.mjs +201 -0
- package/scripts/moltbook-cli.mjs +1302 -0
- package/scripts/moltbook-scout-run.sh +14 -0
- package/scripts/moltbook-watcher.mjs +294 -0
- package/scripts/viveworker-bridge.mjs +548 -12
- package/scripts/viveworker-claude-hook.mjs +7 -1
- package/scripts/viveworker.mjs +303 -1
- package/web/app.css +75 -1
- package/web/app.js +298 -25
- package/web/i18n.js +55 -1
- package/web/sw.js +1 -1
|
@@ -26,7 +26,7 @@ const sessionCookieName = "viveworker_session";
|
|
|
26
26
|
const deviceCookieName = "viveworker_device";
|
|
27
27
|
const historyKinds = new Set(["completion", "plan_ready", "approval", "plan", "choice", "info"]);
|
|
28
28
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
29
|
-
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "completion", "plan_ready", "file_event"]);
|
|
29
|
+
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "completion", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft"]);
|
|
30
30
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
31
31
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
32
32
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -79,6 +79,8 @@ const runtime = {
|
|
|
79
79
|
userInputRequestsByToken: new Map(),
|
|
80
80
|
userInputRequestsByRequestKey: new Map(),
|
|
81
81
|
completionDetailsByToken: new Map(),
|
|
82
|
+
moltbookItemsByToken: new Map(),
|
|
83
|
+
moltbookDraftsByToken: new Map(),
|
|
82
84
|
planDetailsByToken: new Map(),
|
|
83
85
|
recentHistoryItems: [],
|
|
84
86
|
recentTimelineEntries: [],
|
|
@@ -212,6 +214,7 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
212
214
|
deviceDetectedLocale: resolved.detectedLocale || null,
|
|
213
215
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
214
216
|
claudeAwayMode: state?.claudeAwayMode === true,
|
|
217
|
+
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
220
|
|
|
@@ -1989,11 +1992,16 @@ function handleSignal() {
|
|
|
1989
1992
|
|
|
1990
1993
|
function normalizeProvider(value) {
|
|
1991
1994
|
const normalized = String(value || "").toLowerCase();
|
|
1992
|
-
|
|
1995
|
+
if (normalized === "claude") return "claude";
|
|
1996
|
+
if (normalized === "moltbook") return "moltbook";
|
|
1997
|
+
return "codex";
|
|
1993
1998
|
}
|
|
1994
1999
|
|
|
1995
2000
|
function providerDisplayName(locale, provider) {
|
|
1996
|
-
|
|
2001
|
+
const p = normalizeProvider(provider);
|
|
2002
|
+
if (p === "claude") return t(locale, "common.claude");
|
|
2003
|
+
if (p === "moltbook") return "Moltbook";
|
|
2004
|
+
return t(locale, "common.codex");
|
|
1997
2005
|
}
|
|
1998
2006
|
|
|
1999
2007
|
function normalizeHistoryItems(rawItems, maxItems) {
|
|
@@ -2174,14 +2182,18 @@ function normalizeTimelineEntry(raw) {
|
|
|
2174
2182
|
formatNotificationBody(messageText, 180) ||
|
|
2175
2183
|
(kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
|
|
2176
2184
|
"";
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
+
const rawProvider = normalizeProvider(raw.provider);
|
|
2186
|
+
const threadLabel =
|
|
2187
|
+
rawProvider === "moltbook"
|
|
2188
|
+
? cleanText(raw.threadLabel ?? "")
|
|
2189
|
+
: preferTitleOnlyJsonThreadLabel(
|
|
2190
|
+
cleanText(raw.threadLabel ?? ""),
|
|
2191
|
+
threadId,
|
|
2192
|
+
raw.messageText,
|
|
2193
|
+
raw.summary,
|
|
2194
|
+
raw.detailText,
|
|
2195
|
+
raw.message
|
|
2196
|
+
);
|
|
2185
2197
|
const title =
|
|
2186
2198
|
cleanText(raw.title ?? "") ||
|
|
2187
2199
|
(kind === "file_event" ? fileEventTitle(DEFAULT_LOCALE, fileEventType) : "") ||
|
|
@@ -2216,6 +2228,19 @@ function normalizeTimelineEntry(raw) {
|
|
|
2216
2228
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
2217
2229
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
2218
2230
|
provider: normalizeProvider(raw.provider),
|
|
2231
|
+
// Moltbook-specific fields — preserved for detail view fallback after
|
|
2232
|
+
// the in-memory draft/item expires.
|
|
2233
|
+
...(raw.draftText != null ? { draftText: cleanText(raw.draftText) } : {}),
|
|
2234
|
+
...(raw.intent != null ? { intent: cleanText(raw.intent) } : {}),
|
|
2235
|
+
...(raw.postId != null ? { postId: cleanText(raw.postId) } : {}),
|
|
2236
|
+
...(raw.postUrl != null ? { postUrl: cleanText(raw.postUrl) } : {}),
|
|
2237
|
+
...(raw.postTitle != null ? { postTitle: cleanText(raw.postTitle) } : {}),
|
|
2238
|
+
...(raw.postAuthor != null ? { postAuthor: cleanText(raw.postAuthor) } : {}),
|
|
2239
|
+
...(raw.postBody != null ? { postBody: cleanText(raw.postBody) } : {}),
|
|
2240
|
+
...(raw.commentAuthor != null ? { commentAuthor: cleanText(raw.commentAuthor) } : {}),
|
|
2241
|
+
...(raw.contextText != null ? { contextText: cleanText(raw.contextText) } : {}),
|
|
2242
|
+
...(raw.draftType != null ? { draftType: cleanText(raw.draftType) } : {}),
|
|
2243
|
+
...(raw.submoltName != null ? { submoltName: cleanText(raw.submoltName) } : {}),
|
|
2219
2244
|
};
|
|
2220
2245
|
}
|
|
2221
2246
|
|
|
@@ -8982,6 +9007,26 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
8982
9007
|
});
|
|
8983
9008
|
}
|
|
8984
9009
|
|
|
9010
|
+
for (const draft of runtime.moltbookDraftsByToken.values()) {
|
|
9011
|
+
if (draft.decision) continue;
|
|
9012
|
+
const title = draft.postTitle ? `draft → ${draft.postTitle}` : "Moltbook draft";
|
|
9013
|
+
items.push({
|
|
9014
|
+
kind: "moltbook_draft",
|
|
9015
|
+
token: draft.token,
|
|
9016
|
+
threadId: "moltbook",
|
|
9017
|
+
threadLabel: "Moltbook",
|
|
9018
|
+
title,
|
|
9019
|
+
summary: draft.contextSummary || String(draft.draftText || "").slice(0, 160),
|
|
9020
|
+
primaryLabel: t(locale, "server.action.review"),
|
|
9021
|
+
createdAtMs: Number(draft.createdAtMs) || now,
|
|
9022
|
+
provider: "moltbook",
|
|
9023
|
+
});
|
|
9024
|
+
}
|
|
9025
|
+
|
|
9026
|
+
// Moltbook reply items intentionally do not appear in the unhandled list.
|
|
9027
|
+
// Reply drafting is delegated to Codex/Claude Desktop via the
|
|
9028
|
+
// `viveworker moltbook` CLI, so they live in the timeline only.
|
|
9029
|
+
|
|
8985
9030
|
return items.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
|
|
8986
9031
|
}
|
|
8987
9032
|
|
|
@@ -9301,7 +9346,7 @@ function buildTimelineThreads(entries, config) {
|
|
|
9301
9346
|
const byThread = new Map();
|
|
9302
9347
|
for (const entry of entries) {
|
|
9303
9348
|
const threadId = cleanText(entry.threadId || "");
|
|
9304
|
-
if (!threadId) {
|
|
9349
|
+
if (!threadId || threadId === "moltbook" || threadId.startsWith("draft:") || (entry.kind || "").startsWith("moltbook_")) {
|
|
9305
9350
|
continue;
|
|
9306
9351
|
}
|
|
9307
9352
|
const preferredLabel =
|
|
@@ -10414,6 +10459,80 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10414
10459
|
return historicalChoice ? buildHistoryDetail(historicalChoice, locale, runtime) : null;
|
|
10415
10460
|
}
|
|
10416
10461
|
|
|
10462
|
+
if (kind === "moltbook_reply") {
|
|
10463
|
+
const item = runtime.moltbookItemsByToken.get(token);
|
|
10464
|
+
// Fall back to the persisted timeline entry so that items which have
|
|
10465
|
+
// been resolved (removed from the pending map) still render their
|
|
10466
|
+
// detail view from history.
|
|
10467
|
+
const entry = item
|
|
10468
|
+
? null
|
|
10469
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === "moltbook_reply" && e.token === token);
|
|
10470
|
+
const source = item || entry;
|
|
10471
|
+
if (!source) return null;
|
|
10472
|
+
const contextText = item
|
|
10473
|
+
? item.contextText || item.summary || ""
|
|
10474
|
+
: entry.messageText || entry.summary || "";
|
|
10475
|
+
const contextHtml = escapeHtml(contextText)
|
|
10476
|
+
.split("\n")
|
|
10477
|
+
.map((line) => `<p>${line}</p>`)
|
|
10478
|
+
.join("");
|
|
10479
|
+
return {
|
|
10480
|
+
kind: "moltbook_reply",
|
|
10481
|
+
token,
|
|
10482
|
+
threadId: source.threadId || "",
|
|
10483
|
+
threadLabel: source.threadLabel || "Moltbook",
|
|
10484
|
+
title: source.title || "Moltbook reply",
|
|
10485
|
+
summary: source.summary || "",
|
|
10486
|
+
messageHtml: contextHtml,
|
|
10487
|
+
postUrl: source.postUrl || "",
|
|
10488
|
+
commentAuthor: source.commentAuthor || "",
|
|
10489
|
+
provider: "moltbook",
|
|
10490
|
+
draftReply: item?.draftReply || "",
|
|
10491
|
+
createdAtMs: source.createdAtMs || Date.now(),
|
|
10492
|
+
readOnly: !item,
|
|
10493
|
+
actions: [],
|
|
10494
|
+
moltbookReplyEnabled: Boolean(item),
|
|
10495
|
+
};
|
|
10496
|
+
}
|
|
10497
|
+
|
|
10498
|
+
if (kind === "moltbook_draft") {
|
|
10499
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
10500
|
+
const entry = draft
|
|
10501
|
+
? null
|
|
10502
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === "moltbook_draft" && e.token === token);
|
|
10503
|
+
const source = draft || entry;
|
|
10504
|
+
if (!source) return null;
|
|
10505
|
+
const draftText = draft ? draft.draftText : entry.draftText || entry.messageText || "";
|
|
10506
|
+
const contextSummary = draft ? draft.contextSummary || "" : entry.summary || "";
|
|
10507
|
+
const messageHtml = escapeHtml(contextSummary)
|
|
10508
|
+
.split("\n")
|
|
10509
|
+
.map((line) => `<p>${line}</p>`)
|
|
10510
|
+
.join("");
|
|
10511
|
+
return {
|
|
10512
|
+
kind: "moltbook_draft",
|
|
10513
|
+
token,
|
|
10514
|
+
threadId: "moltbook",
|
|
10515
|
+
threadLabel: "Moltbook",
|
|
10516
|
+
title: source.title || "Moltbook draft",
|
|
10517
|
+
summary: source.summary || contextSummary || "",
|
|
10518
|
+
messageHtml,
|
|
10519
|
+
provider: "moltbook",
|
|
10520
|
+
draftText,
|
|
10521
|
+
postId: draft?.postId || entry?.postId || "",
|
|
10522
|
+
postUrl: draft?.postUrl || entry?.postUrl || "",
|
|
10523
|
+
postTitle: draft?.postTitle || entry?.postTitle || "",
|
|
10524
|
+
postAuthor: draft?.postAuthor || entry?.postAuthor || "",
|
|
10525
|
+
postBody: draft?.postBody || entry?.postBody || "",
|
|
10526
|
+
draftType: draft?.draftType || entry?.draftType || "reply",
|
|
10527
|
+
submoltName: draft?.submoltName || entry?.submoltName || "",
|
|
10528
|
+
intent: draft?.intent || entry?.intent || "",
|
|
10529
|
+
createdAtMs: source.createdAtMs || Date.now(),
|
|
10530
|
+
readOnly: !draft || Boolean(draft?.decision),
|
|
10531
|
+
actions: [],
|
|
10532
|
+
moltbookDraftEnabled: Boolean(draft) && !draft.decision,
|
|
10533
|
+
};
|
|
10534
|
+
}
|
|
10535
|
+
|
|
10417
10536
|
const historyItem = historyItemByToken(runtime, kind, token);
|
|
10418
10537
|
return historyItem ? buildHistoryDetail(historyItem, locale, runtime) : null;
|
|
10419
10538
|
}
|
|
@@ -10799,6 +10918,96 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10799
10918
|
return writeJson(res, 200, { ok: true, enabled });
|
|
10800
10919
|
}
|
|
10801
10920
|
|
|
10921
|
+
if (url.pathname === "/api/moltbook/scout-status" && req.method === "GET") {
|
|
10922
|
+
const session = requireApiSession(req, res, config, state);
|
|
10923
|
+
if (!session) {
|
|
10924
|
+
return;
|
|
10925
|
+
}
|
|
10926
|
+
if (!config.moltbookApiKey) {
|
|
10927
|
+
return writeJson(res, 200, { enabled: false });
|
|
10928
|
+
}
|
|
10929
|
+
try {
|
|
10930
|
+
const { readScoutState, rollScoutDayIfNeeded } = await import("./moltbook-api.mjs");
|
|
10931
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
10932
|
+
const batch = scoutState.batch;
|
|
10933
|
+
const batchInfo = batch && batch.candidates?.length ? {
|
|
10934
|
+
collecting: true,
|
|
10935
|
+
candidateCount: batch.candidates.length,
|
|
10936
|
+
topScore: Math.max(...batch.candidates.map((c) => c.score || 0)),
|
|
10937
|
+
remainingSeconds: Math.max(0, Math.round(((batch.startedAt || 0) + (batch.windowMs || 1800000) - Date.now()) / 1000)),
|
|
10938
|
+
} : null;
|
|
10939
|
+
return writeJson(res, 200, {
|
|
10940
|
+
enabled: true,
|
|
10941
|
+
day: scoutState.day,
|
|
10942
|
+
sentToday: scoutState.sentToday,
|
|
10943
|
+
maxDaily: 5,
|
|
10944
|
+
seenPostCount: Object.keys(scoutState.seenPostIds || {}).length,
|
|
10945
|
+
batch: batchInfo,
|
|
10946
|
+
composedToday: scoutState.composedToday || 0,
|
|
10947
|
+
composeSlotsAttempted: Array.isArray(scoutState.composeSlotsAttempted) ? scoutState.composeSlotsAttempted : [],
|
|
10948
|
+
recentComposeTitles: Array.isArray(scoutState.recentComposeTitles) ? scoutState.recentComposeTitles : [],
|
|
10949
|
+
});
|
|
10950
|
+
} catch {
|
|
10951
|
+
return writeJson(res, 200, { enabled: true, day: "", sentToday: 0, maxDaily: 5, seenPostCount: 0 });
|
|
10952
|
+
}
|
|
10953
|
+
}
|
|
10954
|
+
|
|
10955
|
+
// Activity summary for compose (original post) drafting.
|
|
10956
|
+
// Supports ?slot=morning|noon|evening and ?date=YYYY-MM-DD for
|
|
10957
|
+
// time-slot-based compose. Defaults to full-day today.
|
|
10958
|
+
if (url.pathname === "/api/providers/moltbook/activity-summary" && req.method === "GET") {
|
|
10959
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
10960
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
10961
|
+
return writeJson(res, 403, { error: "forbidden" });
|
|
10962
|
+
}
|
|
10963
|
+
const slot = String(url.searchParams.get("slot") || "").toLowerCase();
|
|
10964
|
+
const dateParam = String(url.searchParams.get("date") || "");
|
|
10965
|
+
const now = new Date();
|
|
10966
|
+
const localHour = now.getHours();
|
|
10967
|
+
|
|
10968
|
+
// Determine time range based on slot.
|
|
10969
|
+
let rangeStart, rangeEnd, rangeDate;
|
|
10970
|
+
if (slot === "morning" && dateParam) {
|
|
10971
|
+
// Morning slot: previous day's activities (dateParam = yesterday).
|
|
10972
|
+
const d = new Date(dateParam + "T00:00:00");
|
|
10973
|
+
rangeStart = d.getTime();
|
|
10974
|
+
rangeEnd = rangeStart + 24 * 60 * 60 * 1000;
|
|
10975
|
+
rangeDate = dateParam;
|
|
10976
|
+
} else if (slot === "noon") {
|
|
10977
|
+
// Noon slot: today 00:00 – 12:00 local.
|
|
10978
|
+
const d = new Date(now); d.setHours(0, 0, 0, 0);
|
|
10979
|
+
rangeStart = d.getTime();
|
|
10980
|
+
const noon = new Date(now); noon.setHours(12, 0, 0, 0);
|
|
10981
|
+
rangeEnd = noon.getTime();
|
|
10982
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
10983
|
+
} else if (slot === "evening") {
|
|
10984
|
+
// Evening slot: today full day.
|
|
10985
|
+
const d = new Date(now); d.setHours(0, 0, 0, 0);
|
|
10986
|
+
rangeStart = d.getTime();
|
|
10987
|
+
rangeEnd = Date.now();
|
|
10988
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
10989
|
+
} else {
|
|
10990
|
+
// Default: today full day (UTC midnight to now).
|
|
10991
|
+
const d = new Date(now);
|
|
10992
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
10993
|
+
rangeStart = d.getTime();
|
|
10994
|
+
rangeEnd = Date.now();
|
|
10995
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
10996
|
+
}
|
|
10997
|
+
|
|
10998
|
+
const relevantKinds = new Set(["file_event", "completion", "plan_ready", "assistant_final"]);
|
|
10999
|
+
const entries = (runtime.recentTimelineEntries || [])
|
|
11000
|
+
.filter((e) => e.createdAtMs >= rangeStart && e.createdAtMs < rangeEnd && relevantKinds.has(e.kind))
|
|
11001
|
+
.map((e) => ({
|
|
11002
|
+
kind: e.kind,
|
|
11003
|
+
title: e.title || "",
|
|
11004
|
+
summary: e.summary || "",
|
|
11005
|
+
threadLabel: e.threadLabel || "",
|
|
11006
|
+
createdAtMs: e.createdAtMs,
|
|
11007
|
+
}));
|
|
11008
|
+
return writeJson(res, 200, { date: rangeDate, slot: slot || "full", entries });
|
|
11009
|
+
}
|
|
11010
|
+
|
|
10802
11011
|
if (url.pathname === "/api/push/status" && req.method === "GET") {
|
|
10803
11012
|
const session = requireApiSession(req, res, config, state);
|
|
10804
11013
|
if (!session) {
|
|
@@ -11135,6 +11344,309 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11135
11344
|
});
|
|
11136
11345
|
}
|
|
11137
11346
|
|
|
11347
|
+
if (url.pathname === "/api/providers/moltbook/events" && req.method === "POST") {
|
|
11348
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11349
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11350
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11351
|
+
}
|
|
11352
|
+
let body;
|
|
11353
|
+
try {
|
|
11354
|
+
body = await parseJsonBody(req);
|
|
11355
|
+
} catch {
|
|
11356
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11357
|
+
}
|
|
11358
|
+
const sourceId = cleanText(body.sourceId || "");
|
|
11359
|
+
if (!sourceId) {
|
|
11360
|
+
return writeJson(res, 400, { error: "missing-sourceId" });
|
|
11361
|
+
}
|
|
11362
|
+
const eventType = String(body.eventType || "reply_request");
|
|
11363
|
+
if (eventType === "resolve") {
|
|
11364
|
+
const token = historyToken(`moltbook:${sourceId}`);
|
|
11365
|
+
runtime.moltbookItemsByToken.delete(token);
|
|
11366
|
+
return writeJson(res, 200, { ok: true });
|
|
11367
|
+
}
|
|
11368
|
+
const token = historyToken(`moltbook:${sourceId}`);
|
|
11369
|
+
const item = {
|
|
11370
|
+
token,
|
|
11371
|
+
sourceId,
|
|
11372
|
+
threadId: cleanText(body.threadId || "moltbook"),
|
|
11373
|
+
threadLabel: cleanText(body.threadLabel || "Moltbook"),
|
|
11374
|
+
title: cleanText(body.title || "Moltbook reply"),
|
|
11375
|
+
summary: cleanText(body.summary || ""),
|
|
11376
|
+
contextText: cleanText(body.contextText || ""),
|
|
11377
|
+
draftReply: cleanText(body.draftReply || ""),
|
|
11378
|
+
callbackUrl: cleanText(body.callbackUrl || ""),
|
|
11379
|
+
postUrl: cleanText(body.postUrl || ""),
|
|
11380
|
+
commentAuthor: cleanText(body.commentAuthor || ""),
|
|
11381
|
+
createdAtMs: Number(body.createdAtMs) || Date.now(),
|
|
11382
|
+
resolved: false,
|
|
11383
|
+
};
|
|
11384
|
+
runtime.moltbookItemsByToken.set(token, item);
|
|
11385
|
+
try {
|
|
11386
|
+
recordTimelineEntry({
|
|
11387
|
+
config,
|
|
11388
|
+
runtime,
|
|
11389
|
+
state,
|
|
11390
|
+
entry: {
|
|
11391
|
+
stableId: `moltbook_reply:${sourceId}`,
|
|
11392
|
+
token,
|
|
11393
|
+
kind: "moltbook_reply",
|
|
11394
|
+
threadId: item.threadId,
|
|
11395
|
+
threadLabel: item.threadLabel,
|
|
11396
|
+
title: item.title,
|
|
11397
|
+
summary: item.summary,
|
|
11398
|
+
messageText: item.contextText,
|
|
11399
|
+
createdAtMs: item.createdAtMs,
|
|
11400
|
+
readOnly: false,
|
|
11401
|
+
provider: "moltbook",
|
|
11402
|
+
},
|
|
11403
|
+
});
|
|
11404
|
+
await saveState(config.stateFile, state);
|
|
11405
|
+
} catch (error) {
|
|
11406
|
+
console.error(`[moltbook-timeline-save] ${error.message}`);
|
|
11407
|
+
}
|
|
11408
|
+
try {
|
|
11409
|
+
await deliverWebPushItem({
|
|
11410
|
+
config,
|
|
11411
|
+
state,
|
|
11412
|
+
kind: "moltbook_reply",
|
|
11413
|
+
token,
|
|
11414
|
+
stableId: `moltbook_reply:${item.sourceId}`,
|
|
11415
|
+
title: item.title,
|
|
11416
|
+
body: item.summary,
|
|
11417
|
+
});
|
|
11418
|
+
} catch (error) {
|
|
11419
|
+
console.error(`[moltbook-push-error] ${error.message}`);
|
|
11420
|
+
}
|
|
11421
|
+
return writeJson(res, 200, { ok: true, token });
|
|
11422
|
+
}
|
|
11423
|
+
|
|
11424
|
+
const apiMoltbookReplyMatch = url.pathname.match(/^\/api\/items\/moltbook\/([^/]+)\/reply$/u);
|
|
11425
|
+
if (apiMoltbookReplyMatch && req.method === "POST") {
|
|
11426
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11427
|
+
if (!session) {
|
|
11428
|
+
return;
|
|
11429
|
+
}
|
|
11430
|
+
const token = decodeURIComponent(apiMoltbookReplyMatch[1]);
|
|
11431
|
+
const item = runtime.moltbookItemsByToken.get(token);
|
|
11432
|
+
if (!item) {
|
|
11433
|
+
return writeJson(res, 404, { error: "item-not-found" });
|
|
11434
|
+
}
|
|
11435
|
+
let body;
|
|
11436
|
+
try {
|
|
11437
|
+
body = await parseJsonBody(req);
|
|
11438
|
+
} catch {
|
|
11439
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11440
|
+
}
|
|
11441
|
+
const action = String(body.action || "send");
|
|
11442
|
+
const text = cleanText(body.text || "");
|
|
11443
|
+
if (action === "send" && !text) {
|
|
11444
|
+
return writeJson(res, 400, { error: "empty-reply" });
|
|
11445
|
+
}
|
|
11446
|
+
if (item.callbackUrl) {
|
|
11447
|
+
try {
|
|
11448
|
+
const callbackRes = await fetch(item.callbackUrl, {
|
|
11449
|
+
method: "POST",
|
|
11450
|
+
headers: {
|
|
11451
|
+
"content-type": "application/json",
|
|
11452
|
+
"x-viveworker-hook-secret": config.sessionSecret || "",
|
|
11453
|
+
},
|
|
11454
|
+
body: JSON.stringify({
|
|
11455
|
+
sourceId: item.sourceId,
|
|
11456
|
+
action,
|
|
11457
|
+
text,
|
|
11458
|
+
}),
|
|
11459
|
+
});
|
|
11460
|
+
if (!callbackRes.ok) {
|
|
11461
|
+
return writeJson(res, 502, { error: `callback-failed-${callbackRes.status}` });
|
|
11462
|
+
}
|
|
11463
|
+
} catch (error) {
|
|
11464
|
+
return writeJson(res, 502, { error: `callback-error: ${error.message}` });
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
item.resolved = true;
|
|
11468
|
+
runtime.moltbookItemsByToken.delete(token);
|
|
11469
|
+
return writeJson(res, 200, { ok: true, action });
|
|
11470
|
+
}
|
|
11471
|
+
|
|
11472
|
+
// ---------------------------------------------------------------
|
|
11473
|
+
// Moltbook scout draft channel
|
|
11474
|
+
//
|
|
11475
|
+
// The standalone scout CLI (`viveworker moltbook propose`) submits
|
|
11476
|
+
// a draft reply via POST /api/providers/moltbook/draft, then long-
|
|
11477
|
+
// polls /api/providers/moltbook/draft/:token/decision until the
|
|
11478
|
+
// phone responds via POST /api/items/moltbook-draft/:token/decision.
|
|
11479
|
+
// ---------------------------------------------------------------
|
|
11480
|
+
|
|
11481
|
+
if (url.pathname === "/api/providers/moltbook/draft" && req.method === "POST") {
|
|
11482
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11483
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11484
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11485
|
+
}
|
|
11486
|
+
let body;
|
|
11487
|
+
try {
|
|
11488
|
+
body = await parseJsonBody(req);
|
|
11489
|
+
} catch {
|
|
11490
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11491
|
+
}
|
|
11492
|
+
const sourceId = cleanText(body.sourceId || "");
|
|
11493
|
+
const postId = cleanText(body.postId || "");
|
|
11494
|
+
const draftText = cleanText(body.draftText || "");
|
|
11495
|
+
const draftType = cleanText(body.draftType || "reply");
|
|
11496
|
+
const submoltName = cleanText(body.submoltName || "");
|
|
11497
|
+
if (!sourceId || !draftText) {
|
|
11498
|
+
return writeJson(res, 400, { error: "missing-sourceId-or-draftText" });
|
|
11499
|
+
}
|
|
11500
|
+
if (draftType === "reply" && !postId) {
|
|
11501
|
+
return writeJson(res, 400, { error: "missing-postId-for-reply" });
|
|
11502
|
+
}
|
|
11503
|
+
const token = historyToken(`moltbook_draft:${sourceId}`);
|
|
11504
|
+
const postTitle = cleanText(body.postTitle || "");
|
|
11505
|
+
const postUrl = cleanText(body.postUrl || `https://www.moltbook.com/post/${postId}`);
|
|
11506
|
+
const contextSummary = cleanText(body.contextSummary || "");
|
|
11507
|
+
const postAuthor = cleanText(body.postAuthor || "");
|
|
11508
|
+
const postBody = typeof body.postBody === "string" ? body.postBody : "";
|
|
11509
|
+
const intent = typeof body.intent === "string" ? body.intent : "";
|
|
11510
|
+
const draft = {
|
|
11511
|
+
token,
|
|
11512
|
+
sourceId,
|
|
11513
|
+
postId,
|
|
11514
|
+
postTitle,
|
|
11515
|
+
postAuthor,
|
|
11516
|
+
postBody,
|
|
11517
|
+
postUrl,
|
|
11518
|
+
parentCommentId: cleanText(body.parentCommentId || ""),
|
|
11519
|
+
draftText,
|
|
11520
|
+
draftType,
|
|
11521
|
+
submoltName,
|
|
11522
|
+
intent,
|
|
11523
|
+
contextSummary,
|
|
11524
|
+
createdAtMs: Date.now(),
|
|
11525
|
+
decisionWaiters: [],
|
|
11526
|
+
decision: null,
|
|
11527
|
+
};
|
|
11528
|
+
runtime.moltbookDraftsByToken.set(token, draft);
|
|
11529
|
+
try {
|
|
11530
|
+
recordTimelineEntry({
|
|
11531
|
+
config,
|
|
11532
|
+
runtime,
|
|
11533
|
+
state,
|
|
11534
|
+
entry: {
|
|
11535
|
+
stableId: `moltbook_draft:${sourceId}`,
|
|
11536
|
+
token,
|
|
11537
|
+
kind: "moltbook_draft",
|
|
11538
|
+
threadId: "moltbook",
|
|
11539
|
+
threadLabel: "Moltbook",
|
|
11540
|
+
title: draftType === "original_post"
|
|
11541
|
+
? (postTitle || "Moltbook new post")
|
|
11542
|
+
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
11543
|
+
summary: contextSummary || String(draftText).slice(0, 160),
|
|
11544
|
+
messageText: contextSummary || draftText,
|
|
11545
|
+
draftText,
|
|
11546
|
+
draftType,
|
|
11547
|
+
submoltName,
|
|
11548
|
+
intent,
|
|
11549
|
+
postId,
|
|
11550
|
+
postUrl,
|
|
11551
|
+
postTitle,
|
|
11552
|
+
postAuthor,
|
|
11553
|
+
postBody,
|
|
11554
|
+
createdAtMs: draft.createdAtMs,
|
|
11555
|
+
readOnly: false,
|
|
11556
|
+
provider: "moltbook",
|
|
11557
|
+
},
|
|
11558
|
+
});
|
|
11559
|
+
await saveState(config.stateFile, state);
|
|
11560
|
+
} catch (error) {
|
|
11561
|
+
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
11562
|
+
}
|
|
11563
|
+
try {
|
|
11564
|
+
await deliverWebPushItem({
|
|
11565
|
+
config,
|
|
11566
|
+
state,
|
|
11567
|
+
kind: "moltbook_draft",
|
|
11568
|
+
token,
|
|
11569
|
+
stableId: `moltbook_draft:${sourceId}`,
|
|
11570
|
+
title: draftType === "original_post"
|
|
11571
|
+
? (postTitle || "Moltbook new post")
|
|
11572
|
+
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
11573
|
+
body: String(draftText).slice(0, 160),
|
|
11574
|
+
});
|
|
11575
|
+
} catch (error) {
|
|
11576
|
+
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
11577
|
+
}
|
|
11578
|
+
return writeJson(res, 200, { ok: true, token });
|
|
11579
|
+
}
|
|
11580
|
+
|
|
11581
|
+
const apiMoltbookDraftDecisionGet = url.pathname.match(
|
|
11582
|
+
/^\/api\/providers\/moltbook\/draft\/([^/]+)\/decision$/u
|
|
11583
|
+
);
|
|
11584
|
+
if (apiMoltbookDraftDecisionGet && req.method === "GET") {
|
|
11585
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11586
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11587
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11588
|
+
}
|
|
11589
|
+
const token = decodeURIComponent(apiMoltbookDraftDecisionGet[1]);
|
|
11590
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
11591
|
+
if (!draft) return writeJson(res, 404, { error: "draft-not-found" });
|
|
11592
|
+
if (draft.decision) {
|
|
11593
|
+
return writeJson(res, 200, { status: "decided", ...draft.decision });
|
|
11594
|
+
}
|
|
11595
|
+
const waitParam = Number(url.searchParams.get("wait")) || 25;
|
|
11596
|
+
const waitMs = Math.min(Math.max(waitParam, 1), 60) * 1000;
|
|
11597
|
+
const decided = await new Promise((resolve) => {
|
|
11598
|
+
const timer = setTimeout(() => {
|
|
11599
|
+
const idx = draft.decisionWaiters.indexOf(resolve);
|
|
11600
|
+
if (idx !== -1) draft.decisionWaiters.splice(idx, 1);
|
|
11601
|
+
resolve(null);
|
|
11602
|
+
}, waitMs);
|
|
11603
|
+
draft.decisionWaiters.push((d) => {
|
|
11604
|
+
clearTimeout(timer);
|
|
11605
|
+
resolve(d);
|
|
11606
|
+
});
|
|
11607
|
+
});
|
|
11608
|
+
if (decided) return writeJson(res, 200, { status: "decided", ...decided });
|
|
11609
|
+
return writeJson(res, 200, { status: "pending" });
|
|
11610
|
+
}
|
|
11611
|
+
|
|
11612
|
+
const apiMoltbookDraftDecide = url.pathname.match(
|
|
11613
|
+
/^\/api\/items\/moltbook-draft\/([^/]+)\/decision$/u
|
|
11614
|
+
);
|
|
11615
|
+
if (apiMoltbookDraftDecide && req.method === "POST") {
|
|
11616
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11617
|
+
if (!session) return;
|
|
11618
|
+
const token = decodeURIComponent(apiMoltbookDraftDecide[1]);
|
|
11619
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
11620
|
+
if (!draft) return writeJson(res, 404, { error: "draft-not-found" });
|
|
11621
|
+
let body;
|
|
11622
|
+
try {
|
|
11623
|
+
body = await parseJsonBody(req);
|
|
11624
|
+
} catch {
|
|
11625
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11626
|
+
}
|
|
11627
|
+
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
11628
|
+
const editedText = cleanText(body.editedText || "");
|
|
11629
|
+
const editedTitle = cleanText(body.editedTitle || "");
|
|
11630
|
+
const decision = {
|
|
11631
|
+
action,
|
|
11632
|
+
text: action === "approve" ? editedText || draft.draftText : "",
|
|
11633
|
+
title: action === "approve" ? editedTitle || draft.postTitle : "",
|
|
11634
|
+
decidedAtMs: Date.now(),
|
|
11635
|
+
};
|
|
11636
|
+
draft.decision = decision;
|
|
11637
|
+
for (const waiter of draft.decisionWaiters.splice(0)) {
|
|
11638
|
+
try {
|
|
11639
|
+
waiter(decision);
|
|
11640
|
+
} catch {
|
|
11641
|
+
// ignore
|
|
11642
|
+
}
|
|
11643
|
+
}
|
|
11644
|
+
// Keep the entry in moltbookDraftsByToken briefly so a slow long-
|
|
11645
|
+
// poll can still pick it up; sweeper below removes it after 2min.
|
|
11646
|
+
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120 * 1000).unref?.();
|
|
11647
|
+
return writeJson(res, 200, { ok: true, action });
|
|
11648
|
+
}
|
|
11649
|
+
|
|
11138
11650
|
if (url.pathname === "/api/inbox" && req.method === "GET") {
|
|
11139
11651
|
const session = requireApiSession(req, res, config, state);
|
|
11140
11652
|
if (!session) {
|
|
@@ -13044,6 +13556,23 @@ function isLoopbackHostname(value) {
|
|
|
13044
13556
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
13045
13557
|
}
|
|
13046
13558
|
|
|
13559
|
+
function readMoltbookEnvKey() {
|
|
13560
|
+
// Fall back to the standalone watcher's env file so the bridge can detect
|
|
13561
|
+
// a configured Moltbook integration without requiring MOLTBOOK_API_KEY to
|
|
13562
|
+
// be duplicated into its launchd plist.
|
|
13563
|
+
try {
|
|
13564
|
+
const envPath = path.join(os.homedir(), ".viveworker", "moltbook.env");
|
|
13565
|
+
const raw = readFileSync(envPath, "utf8");
|
|
13566
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
13567
|
+
const m = line.match(/^\s*MOLTBOOK_API_KEY\s*=\s*(.+?)\s*$/u);
|
|
13568
|
+
if (m) return m[1].replace(/^['"]|['"]$/gu, "");
|
|
13569
|
+
}
|
|
13570
|
+
} catch {
|
|
13571
|
+
// ignore
|
|
13572
|
+
}
|
|
13573
|
+
return "";
|
|
13574
|
+
}
|
|
13575
|
+
|
|
13047
13576
|
function buildConfig(cli) {
|
|
13048
13577
|
const codexHome = resolvePath(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
13049
13578
|
const stateFile = resolvePath(process.env.STATE_FILE || path.join(workspaceRoot, ".viveworker-state.json"));
|
|
@@ -13051,6 +13580,7 @@ function buildConfig(cli) {
|
|
|
13051
13580
|
dryRun: cli.dryRun || truthy(process.env.DRY_RUN),
|
|
13052
13581
|
once: cli.once,
|
|
13053
13582
|
codexHome,
|
|
13583
|
+
moltbookApiKey: cleanText(process.env.MOLTBOOK_API_KEY || readMoltbookEnvKey() || ""),
|
|
13054
13584
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
13055
13585
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
13056
13586
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -13844,6 +14374,12 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
13844
14374
|
|
|
13845
14375
|
const nextTimelineEntries = normalizeTimelineEntries(
|
|
13846
14376
|
runtime.recentTimelineEntries.map((entry) => {
|
|
14377
|
+
// Moltbook entries carry their own author-provided title/threadLabel
|
|
14378
|
+
// (e.g. "@broanbot commented"). Skip the native-thread relabel pass so
|
|
14379
|
+
// we don't overwrite them with the UUID-head fallback.
|
|
14380
|
+
if (normalizeProvider(entry.provider) === "moltbook") {
|
|
14381
|
+
return entry;
|
|
14382
|
+
}
|
|
13847
14383
|
const threadId = cleanText(entry.threadId || "");
|
|
13848
14384
|
if (!threadId) {
|
|
13849
14385
|
return entry;
|
|
@@ -181,7 +181,13 @@ async function handlePermissionRequest() {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const result = await postEvent("PermissionRequest", body, APPROVAL_TIMEOUT_MS);
|
|
184
|
-
|
|
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 };
|