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
|
@@ -16,6 +16,8 @@ import webPush from "web-push";
|
|
|
16
16
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
17
17
|
import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
|
|
18
18
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
19
|
+
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
20
|
+
import { registerWithRelay, startRelayPolling, stopRelayPolling, postRelayResult } from "./a2a-relay-client.mjs";
|
|
19
21
|
|
|
20
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
23
|
const __dirname = path.dirname(__filename);
|
|
@@ -26,7 +28,7 @@ const sessionCookieName = "viveworker_session";
|
|
|
26
28
|
const deviceCookieName = "viveworker_device";
|
|
27
29
|
const historyKinds = new Set(["completion", "plan_ready", "approval", "plan", "choice", "info"]);
|
|
28
30
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
29
|
-
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "completion", "plan_ready", "file_event"]);
|
|
31
|
+
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "completion", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft"]);
|
|
30
32
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
31
33
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
32
34
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -39,6 +41,7 @@ const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
|
39
41
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
40
42
|
const envFile = resolveEnvFile(cli.envFile);
|
|
41
43
|
loadEnvFile(envFile);
|
|
44
|
+
loadEnvFile(path.join(os.homedir(), ".viveworker", "a2a.env"));
|
|
42
45
|
await maybeRotateStartupPairingEnv(envFile);
|
|
43
46
|
|
|
44
47
|
const config = buildConfig(cli);
|
|
@@ -79,6 +82,9 @@ const runtime = {
|
|
|
79
82
|
userInputRequestsByToken: new Map(),
|
|
80
83
|
userInputRequestsByRequestKey: new Map(),
|
|
81
84
|
completionDetailsByToken: new Map(),
|
|
85
|
+
moltbookItemsByToken: new Map(),
|
|
86
|
+
moltbookDraftsByToken: new Map(),
|
|
87
|
+
a2aTasksByToken: new Map(),
|
|
82
88
|
planDetailsByToken: new Map(),
|
|
83
89
|
recentHistoryItems: [],
|
|
84
90
|
recentTimelineEntries: [],
|
|
@@ -212,6 +218,9 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
212
218
|
deviceDetectedLocale: resolved.detectedLocale || null,
|
|
213
219
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
214
220
|
claudeAwayMode: state?.claudeAwayMode === true,
|
|
221
|
+
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
222
|
+
a2aEnabled: Boolean(config.a2aApiKey),
|
|
223
|
+
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
215
224
|
};
|
|
216
225
|
}
|
|
217
226
|
|
|
@@ -280,6 +289,8 @@ function notificationIconPrefix(kind) {
|
|
|
280
289
|
return "☑️";
|
|
281
290
|
case "completion":
|
|
282
291
|
return "✅";
|
|
292
|
+
case "a2a_task":
|
|
293
|
+
return "🤝";
|
|
283
294
|
default:
|
|
284
295
|
return "";
|
|
285
296
|
}
|
|
@@ -1989,11 +2000,18 @@ function handleSignal() {
|
|
|
1989
2000
|
|
|
1990
2001
|
function normalizeProvider(value) {
|
|
1991
2002
|
const normalized = String(value || "").toLowerCase();
|
|
1992
|
-
|
|
2003
|
+
if (normalized === "claude") return "claude";
|
|
2004
|
+
if (normalized === "moltbook") return "moltbook";
|
|
2005
|
+
if (normalized === "a2a") return "a2a";
|
|
2006
|
+
return "codex";
|
|
1993
2007
|
}
|
|
1994
2008
|
|
|
1995
2009
|
function providerDisplayName(locale, provider) {
|
|
1996
|
-
|
|
2010
|
+
const p = normalizeProvider(provider);
|
|
2011
|
+
if (p === "claude") return t(locale, "common.claude");
|
|
2012
|
+
if (p === "moltbook") return "Moltbook";
|
|
2013
|
+
if (p === "a2a") return "A2A";
|
|
2014
|
+
return t(locale, "common.codex");
|
|
1997
2015
|
}
|
|
1998
2016
|
|
|
1999
2017
|
function normalizeHistoryItems(rawItems, maxItems) {
|
|
@@ -2085,17 +2103,31 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2085
2103
|
}
|
|
2086
2104
|
return timelineKindSortPriority(right.kind) - timelineKindSortPriority(left.kind);
|
|
2087
2105
|
});
|
|
2106
|
+
// Per-provider eviction: each provider (codex, claude, moltbook) gets up
|
|
2107
|
+
// to `maxItems` entries, so entries from one busy provider don't push out
|
|
2108
|
+
// entries from another. Total capacity is maxItems * providerCount.
|
|
2088
2109
|
const deduped = [];
|
|
2089
2110
|
const seen = new Set();
|
|
2111
|
+
const perProviderCount = {};
|
|
2112
|
+
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a"]);
|
|
2113
|
+
const saturatedProviders = new Set();
|
|
2090
2114
|
for (const item of normalized) {
|
|
2091
2115
|
if (seen.has(item.stableId)) {
|
|
2092
2116
|
continue;
|
|
2093
2117
|
}
|
|
2118
|
+
const prov = item.provider || "codex";
|
|
2119
|
+
if (saturatedProviders.has(prov)) {
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
const count = (perProviderCount[prov] || 0) + 1;
|
|
2123
|
+
perProviderCount[prov] = count;
|
|
2124
|
+
if (count > maxItems) {
|
|
2125
|
+
saturatedProviders.add(prov);
|
|
2126
|
+
if (saturatedProviders.size >= knownProviders.size) break;
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2094
2129
|
seen.add(item.stableId);
|
|
2095
2130
|
deduped.push(item);
|
|
2096
|
-
if (deduped.length >= maxItems) {
|
|
2097
|
-
break;
|
|
2098
|
-
}
|
|
2099
2131
|
}
|
|
2100
2132
|
return deduped;
|
|
2101
2133
|
}
|
|
@@ -2174,14 +2206,18 @@ function normalizeTimelineEntry(raw) {
|
|
|
2174
2206
|
formatNotificationBody(messageText, 180) ||
|
|
2175
2207
|
(kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
|
|
2176
2208
|
"";
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2209
|
+
const rawProvider = normalizeProvider(raw.provider);
|
|
2210
|
+
const threadLabel =
|
|
2211
|
+
rawProvider === "moltbook"
|
|
2212
|
+
? cleanText(raw.threadLabel ?? "")
|
|
2213
|
+
: preferTitleOnlyJsonThreadLabel(
|
|
2214
|
+
cleanText(raw.threadLabel ?? ""),
|
|
2215
|
+
threadId,
|
|
2216
|
+
raw.messageText,
|
|
2217
|
+
raw.summary,
|
|
2218
|
+
raw.detailText,
|
|
2219
|
+
raw.message
|
|
2220
|
+
);
|
|
2185
2221
|
const title =
|
|
2186
2222
|
cleanText(raw.title ?? "") ||
|
|
2187
2223
|
(kind === "file_event" ? fileEventTitle(DEFAULT_LOCALE, fileEventType) : "") ||
|
|
@@ -2216,6 +2252,20 @@ function normalizeTimelineEntry(raw) {
|
|
|
2216
2252
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
2217
2253
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
2218
2254
|
provider: normalizeProvider(raw.provider),
|
|
2255
|
+
// Moltbook-specific fields — preserved for detail view fallback after
|
|
2256
|
+
// the in-memory draft/item expires.
|
|
2257
|
+
...(raw.draftText != null ? { draftText: cleanText(raw.draftText) } : {}),
|
|
2258
|
+
...(raw.intent != null ? { intent: cleanText(raw.intent) } : {}),
|
|
2259
|
+
...(raw.postId != null ? { postId: cleanText(raw.postId) } : {}),
|
|
2260
|
+
...(raw.postUrl != null ? { postUrl: cleanText(raw.postUrl) } : {}),
|
|
2261
|
+
...(raw.postTitle != null ? { postTitle: cleanText(raw.postTitle) } : {}),
|
|
2262
|
+
...(raw.postAuthor != null ? { postAuthor: cleanText(raw.postAuthor) } : {}),
|
|
2263
|
+
...(raw.postBody != null ? { postBody: cleanText(raw.postBody) } : {}),
|
|
2264
|
+
...(raw.commentAuthor != null ? { commentAuthor: cleanText(raw.commentAuthor) } : {}),
|
|
2265
|
+
...(raw.contextText != null ? { contextText: cleanText(raw.contextText) } : {}),
|
|
2266
|
+
...(raw.draftType != null ? { draftType: cleanText(raw.draftType) } : {}),
|
|
2267
|
+
...(raw.submoltName != null ? { submoltName: cleanText(raw.submoltName) } : {}),
|
|
2268
|
+
...(raw.slot != null ? { slot: cleanText(raw.slot) } : {}),
|
|
2219
2269
|
};
|
|
2220
2270
|
}
|
|
2221
2271
|
|
|
@@ -3066,7 +3116,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3066
3116
|
const defaultThreadLabel = path.basename(decodedProjectDir) || "";
|
|
3067
3117
|
|
|
3068
3118
|
// On startup, use the same "recent only" strategy as processRolloutFile to avoid
|
|
3069
|
-
// re-processing old entries that would be immediately discarded by the
|
|
3119
|
+
// re-processing old entries that would be immediately discarded by the per-provider entry limit.
|
|
3070
3120
|
let initialOffset = 0;
|
|
3071
3121
|
let startupCutoffMs = 0;
|
|
3072
3122
|
try {
|
|
@@ -3179,6 +3229,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3179
3229
|
}
|
|
3180
3230
|
text = cleanText(text);
|
|
3181
3231
|
if (!text) continue;
|
|
3232
|
+
// Skip Claude Code internal system messages (task notifications, system
|
|
3233
|
+
// reminders, etc.) — these are XML-like tags injected into the
|
|
3234
|
+
// conversation that should not appear on the user-facing timeline.
|
|
3235
|
+
if (/^<(?:task-notification|system-reminder)\b/i.test(text.trim())) continue;
|
|
3182
3236
|
|
|
3183
3237
|
const kind = type === "user" ? "user_message" : "assistant_final";
|
|
3184
3238
|
const stableId = `${kind}:${threadId}:${uuid}`;
|
|
@@ -8982,6 +9036,41 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
8982
9036
|
});
|
|
8983
9037
|
}
|
|
8984
9038
|
|
|
9039
|
+
for (const draft of runtime.moltbookDraftsByToken.values()) {
|
|
9040
|
+
if (draft.decision) continue;
|
|
9041
|
+
const title = draft.postTitle ? `draft → ${draft.postTitle}` : "Moltbook draft";
|
|
9042
|
+
items.push({
|
|
9043
|
+
kind: "moltbook_draft",
|
|
9044
|
+
token: draft.token,
|
|
9045
|
+
threadId: "moltbook",
|
|
9046
|
+
threadLabel: "Moltbook",
|
|
9047
|
+
title,
|
|
9048
|
+
summary: draft.contextSummary || String(draft.draftText || "").slice(0, 160),
|
|
9049
|
+
primaryLabel: t(locale, "server.action.review"),
|
|
9050
|
+
createdAtMs: Number(draft.createdAtMs) || now,
|
|
9051
|
+
provider: "moltbook",
|
|
9052
|
+
});
|
|
9053
|
+
}
|
|
9054
|
+
|
|
9055
|
+
for (const task of runtime.a2aTasksByToken.values()) {
|
|
9056
|
+
if (task.status !== "submitted") continue;
|
|
9057
|
+
items.push({
|
|
9058
|
+
kind: "a2a_task",
|
|
9059
|
+
token: task.token,
|
|
9060
|
+
threadId: "a2a",
|
|
9061
|
+
threadLabel: "A2A",
|
|
9062
|
+
title: `A2A: ${cleanText(task.instruction || "").slice(0, 80)}`,
|
|
9063
|
+
summary: cleanText(task.instruction || "").slice(0, 160),
|
|
9064
|
+
primaryLabel: t(locale, "server.action.review"),
|
|
9065
|
+
createdAtMs: Number(task.createdAtMs) || now,
|
|
9066
|
+
provider: "a2a",
|
|
9067
|
+
});
|
|
9068
|
+
}
|
|
9069
|
+
|
|
9070
|
+
// Moltbook reply items intentionally do not appear in the unhandled list.
|
|
9071
|
+
// Reply drafting is delegated to Codex/Claude Desktop via the
|
|
9072
|
+
// `viveworker moltbook` CLI, so they live in the timeline only.
|
|
9073
|
+
|
|
8985
9074
|
return items.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
|
|
8986
9075
|
}
|
|
8987
9076
|
|
|
@@ -9301,7 +9390,7 @@ function buildTimelineThreads(entries, config) {
|
|
|
9301
9390
|
const byThread = new Map();
|
|
9302
9391
|
for (const entry of entries) {
|
|
9303
9392
|
const threadId = cleanText(entry.threadId || "");
|
|
9304
|
-
if (!threadId) {
|
|
9393
|
+
if (!threadId || threadId === "moltbook" || threadId.startsWith("draft:") || (entry.kind || "").startsWith("moltbook_")) {
|
|
9305
9394
|
continue;
|
|
9306
9395
|
}
|
|
9307
9396
|
const preferredLabel =
|
|
@@ -10414,6 +10503,113 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10414
10503
|
return historicalChoice ? buildHistoryDetail(historicalChoice, locale, runtime) : null;
|
|
10415
10504
|
}
|
|
10416
10505
|
|
|
10506
|
+
if (kind === "moltbook_reply") {
|
|
10507
|
+
const item = runtime.moltbookItemsByToken.get(token);
|
|
10508
|
+
// Fall back to the persisted timeline entry so that items which have
|
|
10509
|
+
// been resolved (removed from the pending map) still render their
|
|
10510
|
+
// detail view from history.
|
|
10511
|
+
const entry = item
|
|
10512
|
+
? null
|
|
10513
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === "moltbook_reply" && e.token === token);
|
|
10514
|
+
const source = item || entry;
|
|
10515
|
+
if (!source) return null;
|
|
10516
|
+
const contextText = item
|
|
10517
|
+
? item.contextText || item.summary || ""
|
|
10518
|
+
: entry.messageText || entry.summary || "";
|
|
10519
|
+
const contextHtml = escapeHtml(contextText)
|
|
10520
|
+
.split("\n")
|
|
10521
|
+
.map((line) => `<p>${line}</p>`)
|
|
10522
|
+
.join("");
|
|
10523
|
+
return {
|
|
10524
|
+
kind: "moltbook_reply",
|
|
10525
|
+
token,
|
|
10526
|
+
threadId: source.threadId || "",
|
|
10527
|
+
threadLabel: source.threadLabel || "Moltbook",
|
|
10528
|
+
title: source.title || "Moltbook reply",
|
|
10529
|
+
summary: source.summary || "",
|
|
10530
|
+
messageHtml: contextHtml,
|
|
10531
|
+
postUrl: source.postUrl || "",
|
|
10532
|
+
commentAuthor: source.commentAuthor || "",
|
|
10533
|
+
provider: "moltbook",
|
|
10534
|
+
draftReply: item?.draftReply || "",
|
|
10535
|
+
createdAtMs: source.createdAtMs || Date.now(),
|
|
10536
|
+
readOnly: !item,
|
|
10537
|
+
actions: [],
|
|
10538
|
+
moltbookReplyEnabled: Boolean(item),
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
10541
|
+
|
|
10542
|
+
if (kind === "moltbook_draft") {
|
|
10543
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
10544
|
+
const entry = draft
|
|
10545
|
+
? null
|
|
10546
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === "moltbook_draft" && e.token === token);
|
|
10547
|
+
const source = draft || entry;
|
|
10548
|
+
if (!source) return null;
|
|
10549
|
+
const draftText = draft ? draft.draftText : entry.draftText || entry.messageText || "";
|
|
10550
|
+
const contextSummary = draft ? draft.contextSummary || "" : entry.summary || "";
|
|
10551
|
+
const messageHtml = escapeHtml(contextSummary)
|
|
10552
|
+
.split("\n")
|
|
10553
|
+
.map((line) => `<p>${line}</p>`)
|
|
10554
|
+
.join("");
|
|
10555
|
+
return {
|
|
10556
|
+
kind: "moltbook_draft",
|
|
10557
|
+
token,
|
|
10558
|
+
threadId: "moltbook",
|
|
10559
|
+
threadLabel: "Moltbook",
|
|
10560
|
+
title: source.title || "Moltbook draft",
|
|
10561
|
+
summary: source.summary || contextSummary || "",
|
|
10562
|
+
messageHtml,
|
|
10563
|
+
provider: "moltbook",
|
|
10564
|
+
draftText,
|
|
10565
|
+
postId: draft?.postId || entry?.postId || "",
|
|
10566
|
+
postUrl: draft?.postUrl || entry?.postUrl || "",
|
|
10567
|
+
postTitle: draft?.postTitle || entry?.postTitle || "",
|
|
10568
|
+
postAuthor: draft?.postAuthor || entry?.postAuthor || "",
|
|
10569
|
+
postBody: draft?.postBody || entry?.postBody || "",
|
|
10570
|
+
draftType: draft?.draftType || entry?.draftType || "reply",
|
|
10571
|
+
submoltName: draft?.submoltName || entry?.submoltName || "",
|
|
10572
|
+
intent: draft?.intent || entry?.intent || "",
|
|
10573
|
+
slot: draft?.slot || entry?.slot || "",
|
|
10574
|
+
createdAtMs: source.createdAtMs || Date.now(),
|
|
10575
|
+
readOnly: !draft || Boolean(draft?.decision),
|
|
10576
|
+
actions: [],
|
|
10577
|
+
moltbookDraftEnabled: Boolean(draft) && !draft.decision,
|
|
10578
|
+
};
|
|
10579
|
+
}
|
|
10580
|
+
|
|
10581
|
+
if (kind === "a2a_task") {
|
|
10582
|
+
const task = runtime.a2aTasksByToken.get(token);
|
|
10583
|
+
const entry = task
|
|
10584
|
+
? null
|
|
10585
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === "a2a_task" && e.token === token);
|
|
10586
|
+
const source = task || entry;
|
|
10587
|
+
if (!source) return null;
|
|
10588
|
+
const instruction = task ? task.instruction : entry.messageText || entry.summary || "";
|
|
10589
|
+
const messageHtml = escapeHtml(instruction)
|
|
10590
|
+
.split("\n")
|
|
10591
|
+
.map((line) => `<p>${line}</p>`)
|
|
10592
|
+
.join("");
|
|
10593
|
+
return {
|
|
10594
|
+
kind: "a2a_task",
|
|
10595
|
+
token,
|
|
10596
|
+
threadId: "a2a",
|
|
10597
|
+
threadLabel: "A2A",
|
|
10598
|
+
title: source.title || `A2A Task`,
|
|
10599
|
+
summary: source.summary || cleanText(instruction).slice(0, 160),
|
|
10600
|
+
messageHtml,
|
|
10601
|
+
provider: "a2a",
|
|
10602
|
+
instruction,
|
|
10603
|
+
taskId: task?.id || "",
|
|
10604
|
+
taskStatus: task?.status || entry?.taskStatus || "unknown",
|
|
10605
|
+
callerInfo: task?.callerInfo || {},
|
|
10606
|
+
createdAtMs: source.createdAtMs || Date.now(),
|
|
10607
|
+
readOnly: !task || task.status !== "submitted",
|
|
10608
|
+
actions: [],
|
|
10609
|
+
a2aTaskEnabled: Boolean(task) && task.status === "submitted",
|
|
10610
|
+
};
|
|
10611
|
+
}
|
|
10612
|
+
|
|
10417
10613
|
const historyItem = historyItemByToken(runtime, kind, token);
|
|
10418
10614
|
return historyItem ? buildHistoryDetail(historyItem, locale, runtime) : null;
|
|
10419
10615
|
}
|
|
@@ -10631,6 +10827,104 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10631
10827
|
}
|
|
10632
10828
|
}
|
|
10633
10829
|
|
|
10830
|
+
// ---------------------------------------------------------------
|
|
10831
|
+
// A2A (Agent2Agent) Protocol endpoints
|
|
10832
|
+
// ---------------------------------------------------------------
|
|
10833
|
+
|
|
10834
|
+
if (url.pathname === "/.well-known/agent.json" && req.method === "GET") {
|
|
10835
|
+
if (!config.a2aApiKey) {
|
|
10836
|
+
return writeJson(res, 404, { error: "a2a-not-configured" });
|
|
10837
|
+
}
|
|
10838
|
+
return writeJson(res, 200, buildAgentCard(config));
|
|
10839
|
+
}
|
|
10840
|
+
|
|
10841
|
+
if (url.pathname === "/a2a" && req.method === "POST") {
|
|
10842
|
+
if (!config.a2aApiKey) {
|
|
10843
|
+
return writeJson(res, 404, { error: "a2a-not-configured" });
|
|
10844
|
+
}
|
|
10845
|
+
let body;
|
|
10846
|
+
try {
|
|
10847
|
+
body = await parseJsonBody(req);
|
|
10848
|
+
} catch {
|
|
10849
|
+
return writeJson(res, 400, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
10850
|
+
}
|
|
10851
|
+
return handleA2ARequest({
|
|
10852
|
+
req, res, body, config, runtime, state,
|
|
10853
|
+
writeJson, recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText,
|
|
10854
|
+
});
|
|
10855
|
+
}
|
|
10856
|
+
|
|
10857
|
+
// A2A task decision (user approves/denies from PWA)
|
|
10858
|
+
const apiA2ATaskDecide = url.pathname.match(/^\/api\/items\/a2a-task\/([^/]+)\/decision$/u);
|
|
10859
|
+
if (apiA2ATaskDecide && req.method === "POST") {
|
|
10860
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
10861
|
+
if (!session) return;
|
|
10862
|
+
const token = decodeURIComponent(apiA2ATaskDecide[1]);
|
|
10863
|
+
const task = runtime.a2aTasksByToken.get(token);
|
|
10864
|
+
if (!task) return writeJson(res, 404, { error: "task-not-found" });
|
|
10865
|
+
let body;
|
|
10866
|
+
try {
|
|
10867
|
+
body = await parseJsonBody(req);
|
|
10868
|
+
} catch {
|
|
10869
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
10870
|
+
}
|
|
10871
|
+
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
10872
|
+
const instruction = cleanText(body.instruction || "");
|
|
10873
|
+
resolveA2ATaskDecision(task, { action, instruction });
|
|
10874
|
+
|
|
10875
|
+
if (action === "approve") {
|
|
10876
|
+
// Execute task asynchronously via Codex
|
|
10877
|
+
(async () => {
|
|
10878
|
+
try {
|
|
10879
|
+
const { executeA2ATask } = await import("./a2a-executor.mjs");
|
|
10880
|
+
await executeA2ATask(task, config, runtime, state, { recordTimelineEntry, saveState });
|
|
10881
|
+
} catch (error) {
|
|
10882
|
+
console.error(`[a2a-execute-error] ${error.message}`);
|
|
10883
|
+
failA2ATask(task, error.message);
|
|
10884
|
+
}
|
|
10885
|
+
// Post result to relay if this is a relay-originated task
|
|
10886
|
+
if (task.relayTaskId) {
|
|
10887
|
+
postRelayResult({ config, task }).catch((err) =>
|
|
10888
|
+
console.error(`[a2a-relay] Result post error: ${err.message}`)
|
|
10889
|
+
);
|
|
10890
|
+
}
|
|
10891
|
+
})();
|
|
10892
|
+
} else if (task.relayTaskId) {
|
|
10893
|
+
// Denied — post rejection back to relay
|
|
10894
|
+
postRelayResult({ config, task }).catch((err) =>
|
|
10895
|
+
console.error(`[a2a-relay] Rejection post error: ${err.message}`)
|
|
10896
|
+
);
|
|
10897
|
+
}
|
|
10898
|
+
|
|
10899
|
+
// Keep entry briefly for polling
|
|
10900
|
+
setTimeout(() => {
|
|
10901
|
+
if (task.status === "rejected" || task.status === "canceled") {
|
|
10902
|
+
runtime.a2aTasksByToken.delete(token);
|
|
10903
|
+
}
|
|
10904
|
+
}, 300_000).unref?.(); // 5 min cleanup for rejected/canceled
|
|
10905
|
+
|
|
10906
|
+
return writeJson(res, 200, { ok: true, action });
|
|
10907
|
+
}
|
|
10908
|
+
|
|
10909
|
+
// A2A task status polling (for external callers, authenticated via A2A key)
|
|
10910
|
+
const apiA2ATaskStatus = url.pathname.match(/^\/api\/providers\/a2a\/tasks\/([^/]+)\/status$/u);
|
|
10911
|
+
if (apiA2ATaskStatus && req.method === "GET") {
|
|
10912
|
+
const apiKey = req.headers["x-a2a-key"] || "";
|
|
10913
|
+
if (!config.a2aApiKey || apiKey !== config.a2aApiKey) {
|
|
10914
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
10915
|
+
}
|
|
10916
|
+
const token = decodeURIComponent(apiA2ATaskStatus[1]);
|
|
10917
|
+
const task = runtime.a2aTasksByToken.get(token);
|
|
10918
|
+
if (!task) return writeJson(res, 404, { error: "task-not-found" });
|
|
10919
|
+
return writeJson(res, 200, {
|
|
10920
|
+
id: task.id,
|
|
10921
|
+
status: task.status,
|
|
10922
|
+
statusMessage: task.statusMessage || "",
|
|
10923
|
+
artifacts: task.artifacts,
|
|
10924
|
+
updatedAtMs: task.updatedAtMs,
|
|
10925
|
+
});
|
|
10926
|
+
}
|
|
10927
|
+
|
|
10634
10928
|
if (url.pathname === "/api/session" && req.method === "GET") {
|
|
10635
10929
|
const session = readSession(req, config, state);
|
|
10636
10930
|
const localeInfo = resolveDeviceLocaleInfo(config, state, session.deviceId);
|
|
@@ -10799,6 +11093,96 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10799
11093
|
return writeJson(res, 200, { ok: true, enabled });
|
|
10800
11094
|
}
|
|
10801
11095
|
|
|
11096
|
+
if (url.pathname === "/api/moltbook/scout-status" && req.method === "GET") {
|
|
11097
|
+
const session = requireApiSession(req, res, config, state);
|
|
11098
|
+
if (!session) {
|
|
11099
|
+
return;
|
|
11100
|
+
}
|
|
11101
|
+
if (!config.moltbookApiKey) {
|
|
11102
|
+
return writeJson(res, 200, { enabled: false });
|
|
11103
|
+
}
|
|
11104
|
+
try {
|
|
11105
|
+
const { readScoutState, rollScoutDayIfNeeded } = await import("./moltbook-api.mjs");
|
|
11106
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
11107
|
+
const batch = scoutState.batch;
|
|
11108
|
+
const batchInfo = batch && batch.candidates?.length ? {
|
|
11109
|
+
collecting: true,
|
|
11110
|
+
candidateCount: batch.candidates.length,
|
|
11111
|
+
topScore: Math.max(...batch.candidates.map((c) => c.score || 0)),
|
|
11112
|
+
remainingSeconds: Math.max(0, Math.round(((batch.startedAt || 0) + (batch.windowMs || 1800000) - Date.now()) / 1000)),
|
|
11113
|
+
} : null;
|
|
11114
|
+
return writeJson(res, 200, {
|
|
11115
|
+
enabled: true,
|
|
11116
|
+
day: scoutState.day,
|
|
11117
|
+
sentToday: scoutState.sentToday,
|
|
11118
|
+
maxDaily: 5,
|
|
11119
|
+
seenPostCount: Object.keys(scoutState.seenPostIds || {}).length,
|
|
11120
|
+
batch: batchInfo,
|
|
11121
|
+
composedToday: scoutState.composedToday || 0,
|
|
11122
|
+
composeSlotsAttempted: Array.isArray(scoutState.composeSlotsAttempted) ? scoutState.composeSlotsAttempted : [],
|
|
11123
|
+
recentComposeTitles: Array.isArray(scoutState.recentComposeTitles) ? scoutState.recentComposeTitles : [],
|
|
11124
|
+
});
|
|
11125
|
+
} catch {
|
|
11126
|
+
return writeJson(res, 200, { enabled: true, day: "", sentToday: 0, maxDaily: 5, seenPostCount: 0 });
|
|
11127
|
+
}
|
|
11128
|
+
}
|
|
11129
|
+
|
|
11130
|
+
// Activity summary for compose (original post) drafting.
|
|
11131
|
+
// Supports ?slot=morning|noon|evening and ?date=YYYY-MM-DD for
|
|
11132
|
+
// time-slot-based compose. Defaults to full-day today.
|
|
11133
|
+
if (url.pathname === "/api/providers/moltbook/activity-summary" && req.method === "GET") {
|
|
11134
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11135
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11136
|
+
return writeJson(res, 403, { error: "forbidden" });
|
|
11137
|
+
}
|
|
11138
|
+
const slot = String(url.searchParams.get("slot") || "").toLowerCase();
|
|
11139
|
+
const dateParam = String(url.searchParams.get("date") || "");
|
|
11140
|
+
const now = new Date();
|
|
11141
|
+
const localHour = now.getHours();
|
|
11142
|
+
|
|
11143
|
+
// Determine time range based on slot.
|
|
11144
|
+
let rangeStart, rangeEnd, rangeDate;
|
|
11145
|
+
if (slot === "morning" && dateParam) {
|
|
11146
|
+
// Morning slot: previous day's activities (dateParam = yesterday).
|
|
11147
|
+
const d = new Date(dateParam + "T00:00:00");
|
|
11148
|
+
rangeStart = d.getTime();
|
|
11149
|
+
rangeEnd = rangeStart + 24 * 60 * 60 * 1000;
|
|
11150
|
+
rangeDate = dateParam;
|
|
11151
|
+
} else if (slot === "noon") {
|
|
11152
|
+
// Noon slot: today 00:00 – 12:00 local.
|
|
11153
|
+
const d = new Date(now); d.setHours(0, 0, 0, 0);
|
|
11154
|
+
rangeStart = d.getTime();
|
|
11155
|
+
const noon = new Date(now); noon.setHours(12, 0, 0, 0);
|
|
11156
|
+
rangeEnd = noon.getTime();
|
|
11157
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
11158
|
+
} else if (slot === "evening") {
|
|
11159
|
+
// Evening slot: today full day.
|
|
11160
|
+
const d = new Date(now); d.setHours(0, 0, 0, 0);
|
|
11161
|
+
rangeStart = d.getTime();
|
|
11162
|
+
rangeEnd = Date.now();
|
|
11163
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
11164
|
+
} else {
|
|
11165
|
+
// Default: today full day (UTC midnight to now).
|
|
11166
|
+
const d = new Date(now);
|
|
11167
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
11168
|
+
rangeStart = d.getTime();
|
|
11169
|
+
rangeEnd = Date.now();
|
|
11170
|
+
rangeDate = d.toISOString().slice(0, 10);
|
|
11171
|
+
}
|
|
11172
|
+
|
|
11173
|
+
const relevantKinds = new Set(["file_event", "completion", "plan_ready", "assistant_final"]);
|
|
11174
|
+
const entries = (runtime.recentTimelineEntries || [])
|
|
11175
|
+
.filter((e) => e.createdAtMs >= rangeStart && e.createdAtMs < rangeEnd && relevantKinds.has(e.kind))
|
|
11176
|
+
.map((e) => ({
|
|
11177
|
+
kind: e.kind,
|
|
11178
|
+
title: e.title || "",
|
|
11179
|
+
summary: e.summary || "",
|
|
11180
|
+
threadLabel: e.threadLabel || "",
|
|
11181
|
+
createdAtMs: e.createdAtMs,
|
|
11182
|
+
}));
|
|
11183
|
+
return writeJson(res, 200, { date: rangeDate, slot: slot || "full", entries });
|
|
11184
|
+
}
|
|
11185
|
+
|
|
10802
11186
|
if (url.pathname === "/api/push/status" && req.method === "GET") {
|
|
10803
11187
|
const session = requireApiSession(req, res, config, state);
|
|
10804
11188
|
if (!session) {
|
|
@@ -11135,6 +11519,324 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11135
11519
|
});
|
|
11136
11520
|
}
|
|
11137
11521
|
|
|
11522
|
+
if (url.pathname === "/api/providers/moltbook/events" && req.method === "POST") {
|
|
11523
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11524
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11525
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11526
|
+
}
|
|
11527
|
+
let body;
|
|
11528
|
+
try {
|
|
11529
|
+
body = await parseJsonBody(req);
|
|
11530
|
+
} catch {
|
|
11531
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11532
|
+
}
|
|
11533
|
+
const sourceId = cleanText(body.sourceId || "");
|
|
11534
|
+
if (!sourceId) {
|
|
11535
|
+
return writeJson(res, 400, { error: "missing-sourceId" });
|
|
11536
|
+
}
|
|
11537
|
+
const eventType = String(body.eventType || "reply_request");
|
|
11538
|
+
if (eventType === "resolve") {
|
|
11539
|
+
const token = historyToken(`moltbook:${sourceId}`);
|
|
11540
|
+
runtime.moltbookItemsByToken.delete(token);
|
|
11541
|
+
return writeJson(res, 200, { ok: true });
|
|
11542
|
+
}
|
|
11543
|
+
const token = historyToken(`moltbook:${sourceId}`);
|
|
11544
|
+
const item = {
|
|
11545
|
+
token,
|
|
11546
|
+
sourceId,
|
|
11547
|
+
threadId: cleanText(body.threadId || "moltbook"),
|
|
11548
|
+
threadLabel: cleanText(body.threadLabel || "Moltbook"),
|
|
11549
|
+
title: cleanText(body.title || "Moltbook reply"),
|
|
11550
|
+
summary: cleanText(body.summary || ""),
|
|
11551
|
+
contextText: cleanText(body.contextText || ""),
|
|
11552
|
+
draftReply: cleanText(body.draftReply || ""),
|
|
11553
|
+
callbackUrl: cleanText(body.callbackUrl || ""),
|
|
11554
|
+
postUrl: cleanText(body.postUrl || ""),
|
|
11555
|
+
commentAuthor: cleanText(body.commentAuthor || ""),
|
|
11556
|
+
createdAtMs: Number(body.createdAtMs) || Date.now(),
|
|
11557
|
+
resolved: false,
|
|
11558
|
+
};
|
|
11559
|
+
runtime.moltbookItemsByToken.set(token, item);
|
|
11560
|
+
try {
|
|
11561
|
+
recordTimelineEntry({
|
|
11562
|
+
config,
|
|
11563
|
+
runtime,
|
|
11564
|
+
state,
|
|
11565
|
+
entry: {
|
|
11566
|
+
stableId: `moltbook_reply:${sourceId}`,
|
|
11567
|
+
token,
|
|
11568
|
+
kind: "moltbook_reply",
|
|
11569
|
+
threadId: item.threadId,
|
|
11570
|
+
threadLabel: item.threadLabel,
|
|
11571
|
+
title: item.title,
|
|
11572
|
+
summary: item.summary,
|
|
11573
|
+
messageText: item.contextText,
|
|
11574
|
+
createdAtMs: item.createdAtMs,
|
|
11575
|
+
readOnly: false,
|
|
11576
|
+
provider: "moltbook",
|
|
11577
|
+
},
|
|
11578
|
+
});
|
|
11579
|
+
await saveState(config.stateFile, state);
|
|
11580
|
+
} catch (error) {
|
|
11581
|
+
console.error(`[moltbook-timeline-save] ${error.message}`);
|
|
11582
|
+
}
|
|
11583
|
+
try {
|
|
11584
|
+
await deliverWebPushItem({
|
|
11585
|
+
config,
|
|
11586
|
+
state,
|
|
11587
|
+
kind: "moltbook_reply",
|
|
11588
|
+
token,
|
|
11589
|
+
stableId: `moltbook_reply:${item.sourceId}`,
|
|
11590
|
+
title: item.title,
|
|
11591
|
+
body: item.summary,
|
|
11592
|
+
});
|
|
11593
|
+
} catch (error) {
|
|
11594
|
+
console.error(`[moltbook-push-error] ${error.message}`);
|
|
11595
|
+
}
|
|
11596
|
+
return writeJson(res, 200, { ok: true, token });
|
|
11597
|
+
}
|
|
11598
|
+
|
|
11599
|
+
const apiMoltbookReplyMatch = url.pathname.match(/^\/api\/items\/moltbook\/([^/]+)\/reply$/u);
|
|
11600
|
+
if (apiMoltbookReplyMatch && req.method === "POST") {
|
|
11601
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11602
|
+
if (!session) {
|
|
11603
|
+
return;
|
|
11604
|
+
}
|
|
11605
|
+
const token = decodeURIComponent(apiMoltbookReplyMatch[1]);
|
|
11606
|
+
const item = runtime.moltbookItemsByToken.get(token);
|
|
11607
|
+
if (!item) {
|
|
11608
|
+
return writeJson(res, 404, { error: "item-not-found" });
|
|
11609
|
+
}
|
|
11610
|
+
let body;
|
|
11611
|
+
try {
|
|
11612
|
+
body = await parseJsonBody(req);
|
|
11613
|
+
} catch {
|
|
11614
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11615
|
+
}
|
|
11616
|
+
const action = String(body.action || "send");
|
|
11617
|
+
const text = cleanText(body.text || "");
|
|
11618
|
+
if (action === "send" && !text) {
|
|
11619
|
+
return writeJson(res, 400, { error: "empty-reply" });
|
|
11620
|
+
}
|
|
11621
|
+
if (item.callbackUrl) {
|
|
11622
|
+
try {
|
|
11623
|
+
const callbackRes = await fetch(item.callbackUrl, {
|
|
11624
|
+
method: "POST",
|
|
11625
|
+
headers: {
|
|
11626
|
+
"content-type": "application/json",
|
|
11627
|
+
"x-viveworker-hook-secret": config.sessionSecret || "",
|
|
11628
|
+
},
|
|
11629
|
+
body: JSON.stringify({
|
|
11630
|
+
sourceId: item.sourceId,
|
|
11631
|
+
action,
|
|
11632
|
+
text,
|
|
11633
|
+
}),
|
|
11634
|
+
});
|
|
11635
|
+
if (!callbackRes.ok) {
|
|
11636
|
+
return writeJson(res, 502, { error: `callback-failed-${callbackRes.status}` });
|
|
11637
|
+
}
|
|
11638
|
+
} catch (error) {
|
|
11639
|
+
return writeJson(res, 502, { error: `callback-error: ${error.message}` });
|
|
11640
|
+
}
|
|
11641
|
+
}
|
|
11642
|
+
item.resolved = true;
|
|
11643
|
+
runtime.moltbookItemsByToken.delete(token);
|
|
11644
|
+
return writeJson(res, 200, { ok: true, action });
|
|
11645
|
+
}
|
|
11646
|
+
|
|
11647
|
+
// ---------------------------------------------------------------
|
|
11648
|
+
// Moltbook scout draft channel
|
|
11649
|
+
//
|
|
11650
|
+
// The standalone scout CLI (`viveworker moltbook propose`) submits
|
|
11651
|
+
// a draft reply via POST /api/providers/moltbook/draft, then long-
|
|
11652
|
+
// polls /api/providers/moltbook/draft/:token/decision until the
|
|
11653
|
+
// phone responds via POST /api/items/moltbook-draft/:token/decision.
|
|
11654
|
+
// ---------------------------------------------------------------
|
|
11655
|
+
|
|
11656
|
+
if (url.pathname === "/api/providers/moltbook/draft" && req.method === "POST") {
|
|
11657
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11658
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11659
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11660
|
+
}
|
|
11661
|
+
let body;
|
|
11662
|
+
try {
|
|
11663
|
+
body = await parseJsonBody(req);
|
|
11664
|
+
} catch {
|
|
11665
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11666
|
+
}
|
|
11667
|
+
const sourceId = cleanText(body.sourceId || "");
|
|
11668
|
+
const postId = cleanText(body.postId || "");
|
|
11669
|
+
const draftText = cleanText(body.draftText || "");
|
|
11670
|
+
const draftType = cleanText(body.draftType || "reply");
|
|
11671
|
+
const submoltName = cleanText(body.submoltName || "");
|
|
11672
|
+
if (!sourceId || !draftText) {
|
|
11673
|
+
return writeJson(res, 400, { error: "missing-sourceId-or-draftText" });
|
|
11674
|
+
}
|
|
11675
|
+
if (draftType === "reply" && !postId) {
|
|
11676
|
+
return writeJson(res, 400, { error: "missing-postId-for-reply" });
|
|
11677
|
+
}
|
|
11678
|
+
const token = historyToken(`moltbook_draft:${sourceId}`);
|
|
11679
|
+
const postTitle = cleanText(body.postTitle || "");
|
|
11680
|
+
const postUrl = cleanText(body.postUrl || `https://www.moltbook.com/post/${postId}`);
|
|
11681
|
+
const contextSummary = cleanText(body.contextSummary || "");
|
|
11682
|
+
const postAuthor = cleanText(body.postAuthor || "");
|
|
11683
|
+
const postBody = typeof body.postBody === "string" ? body.postBody : "";
|
|
11684
|
+
const intent = typeof body.intent === "string" ? body.intent : "";
|
|
11685
|
+
const slot = cleanText(body.slot || "");
|
|
11686
|
+
const draft = {
|
|
11687
|
+
token,
|
|
11688
|
+
sourceId,
|
|
11689
|
+
postId,
|
|
11690
|
+
postTitle,
|
|
11691
|
+
postAuthor,
|
|
11692
|
+
postBody,
|
|
11693
|
+
postUrl,
|
|
11694
|
+
parentCommentId: cleanText(body.parentCommentId || ""),
|
|
11695
|
+
draftText,
|
|
11696
|
+
draftType,
|
|
11697
|
+
submoltName,
|
|
11698
|
+
intent,
|
|
11699
|
+
slot,
|
|
11700
|
+
contextSummary,
|
|
11701
|
+
createdAtMs: Date.now(),
|
|
11702
|
+
decisionWaiters: [],
|
|
11703
|
+
decision: null,
|
|
11704
|
+
};
|
|
11705
|
+
runtime.moltbookDraftsByToken.set(token, draft);
|
|
11706
|
+
try {
|
|
11707
|
+
recordTimelineEntry({
|
|
11708
|
+
config,
|
|
11709
|
+
runtime,
|
|
11710
|
+
state,
|
|
11711
|
+
entry: {
|
|
11712
|
+
stableId: `moltbook_draft:${sourceId}`,
|
|
11713
|
+
token,
|
|
11714
|
+
kind: "moltbook_draft",
|
|
11715
|
+
threadId: "moltbook",
|
|
11716
|
+
threadLabel: "Moltbook",
|
|
11717
|
+
title: draftType === "original_post"
|
|
11718
|
+
? (postTitle || "Moltbook new post")
|
|
11719
|
+
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
11720
|
+
summary: contextSummary || String(draftText).slice(0, 160),
|
|
11721
|
+
messageText: contextSummary || draftText,
|
|
11722
|
+
draftText,
|
|
11723
|
+
draftType,
|
|
11724
|
+
submoltName,
|
|
11725
|
+
intent,
|
|
11726
|
+
slot,
|
|
11727
|
+
postId,
|
|
11728
|
+
postUrl,
|
|
11729
|
+
postTitle,
|
|
11730
|
+
postAuthor,
|
|
11731
|
+
postBody,
|
|
11732
|
+
createdAtMs: draft.createdAtMs,
|
|
11733
|
+
readOnly: false,
|
|
11734
|
+
provider: "moltbook",
|
|
11735
|
+
},
|
|
11736
|
+
});
|
|
11737
|
+
await saveState(config.stateFile, state);
|
|
11738
|
+
} catch (error) {
|
|
11739
|
+
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
11740
|
+
}
|
|
11741
|
+
try {
|
|
11742
|
+
const pushTitle = (() => {
|
|
11743
|
+
if (draftType !== "original_post") {
|
|
11744
|
+
return postTitle ? `draft → ${postTitle}` : "Moltbook draft";
|
|
11745
|
+
}
|
|
11746
|
+
const greetings = {
|
|
11747
|
+
morning: { en: "Good morning! Share yesterday's highlights?", ja: "おはようございます!昨日の成果をシェアしませんか?" },
|
|
11748
|
+
noon: { en: "Nice progress! Ready to share your morning work?", ja: "午前中お疲れ様でした!進捗をシェアしませんか?" },
|
|
11749
|
+
evening: { en: "Great work today! Let's share what you built.", ja: "今日もお疲れ様でした!成果をシェアしませんか?" },
|
|
11750
|
+
};
|
|
11751
|
+
const locale = config.locale || "en";
|
|
11752
|
+
const lang = locale.startsWith("ja") ? "ja" : "en";
|
|
11753
|
+
const g = greetings[slot];
|
|
11754
|
+
return g ? g[lang] : (postTitle || "Moltbook new post");
|
|
11755
|
+
})();
|
|
11756
|
+
await deliverWebPushItem({
|
|
11757
|
+
config,
|
|
11758
|
+
state,
|
|
11759
|
+
kind: "moltbook_draft",
|
|
11760
|
+
token,
|
|
11761
|
+
stableId: `moltbook_draft:${sourceId}`,
|
|
11762
|
+
title: pushTitle,
|
|
11763
|
+
body: draftType === "original_post" ? (postTitle || String(draftText).slice(0, 160)) : String(draftText).slice(0, 160),
|
|
11764
|
+
});
|
|
11765
|
+
} catch (error) {
|
|
11766
|
+
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
11767
|
+
}
|
|
11768
|
+
return writeJson(res, 200, { ok: true, token });
|
|
11769
|
+
}
|
|
11770
|
+
|
|
11771
|
+
const apiMoltbookDraftDecisionGet = url.pathname.match(
|
|
11772
|
+
/^\/api\/providers\/moltbook\/draft\/([^/]+)\/decision$/u
|
|
11773
|
+
);
|
|
11774
|
+
if (apiMoltbookDraftDecisionGet && req.method === "GET") {
|
|
11775
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
11776
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
11777
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
11778
|
+
}
|
|
11779
|
+
const token = decodeURIComponent(apiMoltbookDraftDecisionGet[1]);
|
|
11780
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
11781
|
+
if (!draft) return writeJson(res, 404, { error: "draft-not-found" });
|
|
11782
|
+
if (draft.decision) {
|
|
11783
|
+
return writeJson(res, 200, { status: "decided", ...draft.decision });
|
|
11784
|
+
}
|
|
11785
|
+
const waitParam = Number(url.searchParams.get("wait")) || 25;
|
|
11786
|
+
const waitMs = Math.min(Math.max(waitParam, 1), 60) * 1000;
|
|
11787
|
+
const decided = await new Promise((resolve) => {
|
|
11788
|
+
const timer = setTimeout(() => {
|
|
11789
|
+
const idx = draft.decisionWaiters.indexOf(resolve);
|
|
11790
|
+
if (idx !== -1) draft.decisionWaiters.splice(idx, 1);
|
|
11791
|
+
resolve(null);
|
|
11792
|
+
}, waitMs);
|
|
11793
|
+
draft.decisionWaiters.push((d) => {
|
|
11794
|
+
clearTimeout(timer);
|
|
11795
|
+
resolve(d);
|
|
11796
|
+
});
|
|
11797
|
+
});
|
|
11798
|
+
if (decided) return writeJson(res, 200, { status: "decided", ...decided });
|
|
11799
|
+
return writeJson(res, 200, { status: "pending" });
|
|
11800
|
+
}
|
|
11801
|
+
|
|
11802
|
+
const apiMoltbookDraftDecide = url.pathname.match(
|
|
11803
|
+
/^\/api\/items\/moltbook-draft\/([^/]+)\/decision$/u
|
|
11804
|
+
);
|
|
11805
|
+
if (apiMoltbookDraftDecide && req.method === "POST") {
|
|
11806
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11807
|
+
if (!session) return;
|
|
11808
|
+
const token = decodeURIComponent(apiMoltbookDraftDecide[1]);
|
|
11809
|
+
const draft = runtime.moltbookDraftsByToken.get(token);
|
|
11810
|
+
if (!draft) return writeJson(res, 404, { error: "draft-not-found" });
|
|
11811
|
+
let body;
|
|
11812
|
+
try {
|
|
11813
|
+
body = await parseJsonBody(req);
|
|
11814
|
+
} catch {
|
|
11815
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11816
|
+
}
|
|
11817
|
+
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
11818
|
+
const editedText = cleanText(body.editedText || "");
|
|
11819
|
+
const editedTitle = cleanText(body.editedTitle || "");
|
|
11820
|
+
const decision = {
|
|
11821
|
+
action,
|
|
11822
|
+
text: action === "approve" ? editedText || draft.draftText : "",
|
|
11823
|
+
title: action === "approve" ? editedTitle || draft.postTitle : "",
|
|
11824
|
+
decidedAtMs: Date.now(),
|
|
11825
|
+
};
|
|
11826
|
+
draft.decision = decision;
|
|
11827
|
+
for (const waiter of draft.decisionWaiters.splice(0)) {
|
|
11828
|
+
try {
|
|
11829
|
+
waiter(decision);
|
|
11830
|
+
} catch {
|
|
11831
|
+
// ignore
|
|
11832
|
+
}
|
|
11833
|
+
}
|
|
11834
|
+
// Keep the entry in moltbookDraftsByToken briefly so a slow long-
|
|
11835
|
+
// poll can still pick it up; sweeper below removes it after 2min.
|
|
11836
|
+
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120 * 1000).unref?.();
|
|
11837
|
+
return writeJson(res, 200, { ok: true, action });
|
|
11838
|
+
}
|
|
11839
|
+
|
|
11138
11840
|
if (url.pathname === "/api/inbox" && req.method === "GET") {
|
|
11139
11841
|
const session = requireApiSession(req, res, config, state);
|
|
11140
11842
|
if (!session) {
|
|
@@ -13044,6 +13746,37 @@ function isLoopbackHostname(value) {
|
|
|
13044
13746
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
13045
13747
|
}
|
|
13046
13748
|
|
|
13749
|
+
function readA2AEnvKey() {
|
|
13750
|
+
try {
|
|
13751
|
+
const envPath = path.join(os.homedir(), ".viveworker", "a2a.env");
|
|
13752
|
+
const raw = readFileSync(envPath, "utf8");
|
|
13753
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
13754
|
+
const m = line.match(/^\s*A2A_API_KEY\s*=\s*(.+?)\s*$/u);
|
|
13755
|
+
if (m) return m[1].replace(/^['"]|['"]$/gu, "");
|
|
13756
|
+
}
|
|
13757
|
+
} catch {
|
|
13758
|
+
// ignore
|
|
13759
|
+
}
|
|
13760
|
+
return "";
|
|
13761
|
+
}
|
|
13762
|
+
|
|
13763
|
+
function readMoltbookEnvKey() {
|
|
13764
|
+
// Fall back to the standalone watcher's env file so the bridge can detect
|
|
13765
|
+
// a configured Moltbook integration without requiring MOLTBOOK_API_KEY to
|
|
13766
|
+
// be duplicated into its launchd plist.
|
|
13767
|
+
try {
|
|
13768
|
+
const envPath = path.join(os.homedir(), ".viveworker", "moltbook.env");
|
|
13769
|
+
const raw = readFileSync(envPath, "utf8");
|
|
13770
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
13771
|
+
const m = line.match(/^\s*MOLTBOOK_API_KEY\s*=\s*(.+?)\s*$/u);
|
|
13772
|
+
if (m) return m[1].replace(/^['"]|['"]$/gu, "");
|
|
13773
|
+
}
|
|
13774
|
+
} catch {
|
|
13775
|
+
// ignore
|
|
13776
|
+
}
|
|
13777
|
+
return "";
|
|
13778
|
+
}
|
|
13779
|
+
|
|
13047
13780
|
function buildConfig(cli) {
|
|
13048
13781
|
const codexHome = resolvePath(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
13049
13782
|
const stateFile = resolvePath(process.env.STATE_FILE || path.join(workspaceRoot, ".viveworker-state.json"));
|
|
@@ -13051,6 +13784,13 @@ function buildConfig(cli) {
|
|
|
13051
13784
|
dryRun: cli.dryRun || truthy(process.env.DRY_RUN),
|
|
13052
13785
|
once: cli.once,
|
|
13053
13786
|
codexHome,
|
|
13787
|
+
moltbookApiKey: cleanText(process.env.MOLTBOOK_API_KEY || readMoltbookEnvKey() || ""),
|
|
13788
|
+
a2aApiKey: cleanText(process.env.A2A_API_KEY || readA2AEnvKey() || ""),
|
|
13789
|
+
a2aPublicUrl: cleanText(process.env.A2A_PUBLIC_URL || ""),
|
|
13790
|
+
a2aRelayUrl: cleanText(process.env.A2A_RELAY_URL || ""),
|
|
13791
|
+
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
13792
|
+
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
13793
|
+
a2aRelayRegisterSecret: cleanText(process.env.A2A_RELAY_REGISTER_SECRET || ""),
|
|
13054
13794
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
13055
13795
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
13056
13796
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -13844,6 +14584,12 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
13844
14584
|
|
|
13845
14585
|
const nextTimelineEntries = normalizeTimelineEntries(
|
|
13846
14586
|
runtime.recentTimelineEntries.map((entry) => {
|
|
14587
|
+
// Moltbook entries carry their own author-provided title/threadLabel
|
|
14588
|
+
// (e.g. "@broanbot commented"). Skip the native-thread relabel pass so
|
|
14589
|
+
// we don't overwrite them with the UUID-head fallback.
|
|
14590
|
+
if (normalizeProvider(entry.provider) === "moltbook") {
|
|
14591
|
+
return entry;
|
|
14592
|
+
}
|
|
13847
14593
|
const threadId = cleanText(entry.threadId || "");
|
|
13848
14594
|
if (!threadId) {
|
|
13849
14595
|
return entry;
|
|
@@ -14418,6 +15164,24 @@ async function main() {
|
|
|
14418
15164
|
}
|
|
14419
15165
|
}
|
|
14420
15166
|
|
|
15167
|
+
// --- A2A Relay ---
|
|
15168
|
+
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
15169
|
+
const regResult = await registerWithRelay({ config, buildAgentCard });
|
|
15170
|
+
if (regResult.ok) {
|
|
15171
|
+
startRelayPolling({
|
|
15172
|
+
config,
|
|
15173
|
+
runtime,
|
|
15174
|
+
state,
|
|
15175
|
+
helpers: { recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText },
|
|
15176
|
+
});
|
|
15177
|
+
} else {
|
|
15178
|
+
console.error(`[a2a-relay] Registration failed: ${regResult.error}`);
|
|
15179
|
+
}
|
|
15180
|
+
}
|
|
15181
|
+
|
|
15182
|
+
let lastA2aEnvCheckAt = 0;
|
|
15183
|
+
const A2A_ENV_CHECK_INTERVAL_MS = 30_000; // check every 30 seconds
|
|
15184
|
+
|
|
14421
15185
|
while (!runtime.stopping) {
|
|
14422
15186
|
try {
|
|
14423
15187
|
const dirty = await scanOnce({ config, runtime, state });
|
|
@@ -14428,6 +15192,38 @@ async function main() {
|
|
|
14428
15192
|
console.error(`[scan-error] ${error.message}`);
|
|
14429
15193
|
}
|
|
14430
15194
|
|
|
15195
|
+
// Hot-reload: detect a2a.env changes and auto-start relay
|
|
15196
|
+
const now = Date.now();
|
|
15197
|
+
if (!config.a2aRelayUrl && now - lastA2aEnvCheckAt > A2A_ENV_CHECK_INTERVAL_MS) {
|
|
15198
|
+
lastA2aEnvCheckAt = now;
|
|
15199
|
+
try {
|
|
15200
|
+
loadEnvFile(path.join(os.homedir(), ".viveworker", "a2a.env"));
|
|
15201
|
+
const newRelayUrl = cleanText(process.env.A2A_RELAY_URL || "");
|
|
15202
|
+
const newRelayUserId = cleanText(process.env.A2A_RELAY_USER_ID || "");
|
|
15203
|
+
const newRelayRegisterSecret = cleanText(process.env.A2A_RELAY_REGISTER_SECRET || "");
|
|
15204
|
+
if (newRelayUrl && newRelayUserId) {
|
|
15205
|
+
config.a2aRelayUrl = newRelayUrl;
|
|
15206
|
+
config.a2aRelayUserId = newRelayUserId;
|
|
15207
|
+
config.a2aRelayRegisterSecret = newRelayRegisterSecret;
|
|
15208
|
+
config.a2aApiKey = cleanText(process.env.A2A_API_KEY || config.a2aApiKey || "");
|
|
15209
|
+
console.log(`[a2a-relay] Detected new relay config, registering...`);
|
|
15210
|
+
const regResult = await registerWithRelay({ config, buildAgentCard });
|
|
15211
|
+
if (regResult.ok) {
|
|
15212
|
+
startRelayPolling({
|
|
15213
|
+
config,
|
|
15214
|
+
runtime,
|
|
15215
|
+
state,
|
|
15216
|
+
helpers: { recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText },
|
|
15217
|
+
});
|
|
15218
|
+
} else {
|
|
15219
|
+
console.error(`[a2a-relay] Registration failed: ${regResult.error}`);
|
|
15220
|
+
}
|
|
15221
|
+
}
|
|
15222
|
+
} catch (error) {
|
|
15223
|
+
// ignore — env file may not exist yet
|
|
15224
|
+
}
|
|
15225
|
+
}
|
|
15226
|
+
|
|
14431
15227
|
if (config.once) {
|
|
14432
15228
|
break;
|
|
14433
15229
|
}
|
|
@@ -14437,6 +15233,8 @@ async function main() {
|
|
|
14437
15233
|
} finally {
|
|
14438
15234
|
runtime.stopping = true;
|
|
14439
15235
|
|
|
15236
|
+
stopRelayPolling();
|
|
15237
|
+
|
|
14440
15238
|
if (runtime.ipcClient) {
|
|
14441
15239
|
runtime.ipcClient.stop();
|
|
14442
15240
|
}
|