viveworker 0.3.0 → 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 +25 -11
- package/package.json +11 -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/moltbook-api.mjs +11 -6
- package/scripts/moltbook-cli.mjs +92 -22
- package/scripts/moltbook-scout-auto.sh +447 -0
- package/scripts/viveworker-bridge.mjs +270 -8
- package/scripts/viveworker.mjs +11 -0
- package/web/app.css +29 -2
- package/web/app.js +152 -8
- package/web/i18n.js +45 -9
- 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);
|
|
@@ -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);
|
|
@@ -81,6 +84,7 @@ const runtime = {
|
|
|
81
84
|
completionDetailsByToken: new Map(),
|
|
82
85
|
moltbookItemsByToken: new Map(),
|
|
83
86
|
moltbookDraftsByToken: new Map(),
|
|
87
|
+
a2aTasksByToken: new Map(),
|
|
84
88
|
planDetailsByToken: new Map(),
|
|
85
89
|
recentHistoryItems: [],
|
|
86
90
|
recentTimelineEntries: [],
|
|
@@ -215,6 +219,8 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
215
219
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
216
220
|
claudeAwayMode: state?.claudeAwayMode === true,
|
|
217
221
|
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
222
|
+
a2aEnabled: Boolean(config.a2aApiKey),
|
|
223
|
+
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
218
224
|
};
|
|
219
225
|
}
|
|
220
226
|
|
|
@@ -283,6 +289,8 @@ function notificationIconPrefix(kind) {
|
|
|
283
289
|
return "☑️";
|
|
284
290
|
case "completion":
|
|
285
291
|
return "✅";
|
|
292
|
+
case "a2a_task":
|
|
293
|
+
return "🤝";
|
|
286
294
|
default:
|
|
287
295
|
return "";
|
|
288
296
|
}
|
|
@@ -1994,6 +2002,7 @@ function normalizeProvider(value) {
|
|
|
1994
2002
|
const normalized = String(value || "").toLowerCase();
|
|
1995
2003
|
if (normalized === "claude") return "claude";
|
|
1996
2004
|
if (normalized === "moltbook") return "moltbook";
|
|
2005
|
+
if (normalized === "a2a") return "a2a";
|
|
1997
2006
|
return "codex";
|
|
1998
2007
|
}
|
|
1999
2008
|
|
|
@@ -2001,6 +2010,7 @@ function providerDisplayName(locale, provider) {
|
|
|
2001
2010
|
const p = normalizeProvider(provider);
|
|
2002
2011
|
if (p === "claude") return t(locale, "common.claude");
|
|
2003
2012
|
if (p === "moltbook") return "Moltbook";
|
|
2013
|
+
if (p === "a2a") return "A2A";
|
|
2004
2014
|
return t(locale, "common.codex");
|
|
2005
2015
|
}
|
|
2006
2016
|
|
|
@@ -2093,17 +2103,31 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2093
2103
|
}
|
|
2094
2104
|
return timelineKindSortPriority(right.kind) - timelineKindSortPriority(left.kind);
|
|
2095
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.
|
|
2096
2109
|
const deduped = [];
|
|
2097
2110
|
const seen = new Set();
|
|
2111
|
+
const perProviderCount = {};
|
|
2112
|
+
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a"]);
|
|
2113
|
+
const saturatedProviders = new Set();
|
|
2098
2114
|
for (const item of normalized) {
|
|
2099
2115
|
if (seen.has(item.stableId)) {
|
|
2100
2116
|
continue;
|
|
2101
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
|
+
}
|
|
2102
2129
|
seen.add(item.stableId);
|
|
2103
2130
|
deduped.push(item);
|
|
2104
|
-
if (deduped.length >= maxItems) {
|
|
2105
|
-
break;
|
|
2106
|
-
}
|
|
2107
2131
|
}
|
|
2108
2132
|
return deduped;
|
|
2109
2133
|
}
|
|
@@ -2241,6 +2265,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
2241
2265
|
...(raw.contextText != null ? { contextText: cleanText(raw.contextText) } : {}),
|
|
2242
2266
|
...(raw.draftType != null ? { draftType: cleanText(raw.draftType) } : {}),
|
|
2243
2267
|
...(raw.submoltName != null ? { submoltName: cleanText(raw.submoltName) } : {}),
|
|
2268
|
+
...(raw.slot != null ? { slot: cleanText(raw.slot) } : {}),
|
|
2244
2269
|
};
|
|
2245
2270
|
}
|
|
2246
2271
|
|
|
@@ -3091,7 +3116,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3091
3116
|
const defaultThreadLabel = path.basename(decodedProjectDir) || "";
|
|
3092
3117
|
|
|
3093
3118
|
// On startup, use the same "recent only" strategy as processRolloutFile to avoid
|
|
3094
|
-
// 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.
|
|
3095
3120
|
let initialOffset = 0;
|
|
3096
3121
|
let startupCutoffMs = 0;
|
|
3097
3122
|
try {
|
|
@@ -3204,6 +3229,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3204
3229
|
}
|
|
3205
3230
|
text = cleanText(text);
|
|
3206
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;
|
|
3207
3236
|
|
|
3208
3237
|
const kind = type === "user" ? "user_message" : "assistant_final";
|
|
3209
3238
|
const stableId = `${kind}:${threadId}:${uuid}`;
|
|
@@ -9023,6 +9052,21 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
9023
9052
|
});
|
|
9024
9053
|
}
|
|
9025
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
|
+
|
|
9026
9070
|
// Moltbook reply items intentionally do not appear in the unhandled list.
|
|
9027
9071
|
// Reply drafting is delegated to Codex/Claude Desktop via the
|
|
9028
9072
|
// `viveworker moltbook` CLI, so they live in the timeline only.
|
|
@@ -10526,6 +10570,7 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10526
10570
|
draftType: draft?.draftType || entry?.draftType || "reply",
|
|
10527
10571
|
submoltName: draft?.submoltName || entry?.submoltName || "",
|
|
10528
10572
|
intent: draft?.intent || entry?.intent || "",
|
|
10573
|
+
slot: draft?.slot || entry?.slot || "",
|
|
10529
10574
|
createdAtMs: source.createdAtMs || Date.now(),
|
|
10530
10575
|
readOnly: !draft || Boolean(draft?.decision),
|
|
10531
10576
|
actions: [],
|
|
@@ -10533,6 +10578,38 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10533
10578
|
};
|
|
10534
10579
|
}
|
|
10535
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
|
+
|
|
10536
10613
|
const historyItem = historyItemByToken(runtime, kind, token);
|
|
10537
10614
|
return historyItem ? buildHistoryDetail(historyItem, locale, runtime) : null;
|
|
10538
10615
|
}
|
|
@@ -10750,6 +10827,104 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10750
10827
|
}
|
|
10751
10828
|
}
|
|
10752
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
|
+
|
|
10753
10928
|
if (url.pathname === "/api/session" && req.method === "GET") {
|
|
10754
10929
|
const session = readSession(req, config, state);
|
|
10755
10930
|
const localeInfo = resolveDeviceLocaleInfo(config, state, session.deviceId);
|
|
@@ -11507,6 +11682,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11507
11682
|
const postAuthor = cleanText(body.postAuthor || "");
|
|
11508
11683
|
const postBody = typeof body.postBody === "string" ? body.postBody : "";
|
|
11509
11684
|
const intent = typeof body.intent === "string" ? body.intent : "";
|
|
11685
|
+
const slot = cleanText(body.slot || "");
|
|
11510
11686
|
const draft = {
|
|
11511
11687
|
token,
|
|
11512
11688
|
sourceId,
|
|
@@ -11520,6 +11696,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11520
11696
|
draftType,
|
|
11521
11697
|
submoltName,
|
|
11522
11698
|
intent,
|
|
11699
|
+
slot,
|
|
11523
11700
|
contextSummary,
|
|
11524
11701
|
createdAtMs: Date.now(),
|
|
11525
11702
|
decisionWaiters: [],
|
|
@@ -11546,6 +11723,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11546
11723
|
draftType,
|
|
11547
11724
|
submoltName,
|
|
11548
11725
|
intent,
|
|
11726
|
+
slot,
|
|
11549
11727
|
postId,
|
|
11550
11728
|
postUrl,
|
|
11551
11729
|
postTitle,
|
|
@@ -11561,16 +11739,28 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11561
11739
|
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
11562
11740
|
}
|
|
11563
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
|
+
})();
|
|
11564
11756
|
await deliverWebPushItem({
|
|
11565
11757
|
config,
|
|
11566
11758
|
state,
|
|
11567
11759
|
kind: "moltbook_draft",
|
|
11568
11760
|
token,
|
|
11569
11761
|
stableId: `moltbook_draft:${sourceId}`,
|
|
11570
|
-
title:
|
|
11571
|
-
|
|
11572
|
-
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
11573
|
-
body: String(draftText).slice(0, 160),
|
|
11762
|
+
title: pushTitle,
|
|
11763
|
+
body: draftType === "original_post" ? (postTitle || String(draftText).slice(0, 160)) : String(draftText).slice(0, 160),
|
|
11574
11764
|
});
|
|
11575
11765
|
} catch (error) {
|
|
11576
11766
|
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
@@ -13556,6 +13746,20 @@ function isLoopbackHostname(value) {
|
|
|
13556
13746
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
13557
13747
|
}
|
|
13558
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
|
+
|
|
13559
13763
|
function readMoltbookEnvKey() {
|
|
13560
13764
|
// Fall back to the standalone watcher's env file so the bridge can detect
|
|
13561
13765
|
// a configured Moltbook integration without requiring MOLTBOOK_API_KEY to
|
|
@@ -13581,6 +13785,12 @@ function buildConfig(cli) {
|
|
|
13581
13785
|
once: cli.once,
|
|
13582
13786
|
codexHome,
|
|
13583
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 || ""),
|
|
13584
13794
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
13585
13795
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
13586
13796
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -14954,6 +15164,24 @@ async function main() {
|
|
|
14954
15164
|
}
|
|
14955
15165
|
}
|
|
14956
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
|
+
|
|
14957
15185
|
while (!runtime.stopping) {
|
|
14958
15186
|
try {
|
|
14959
15187
|
const dirty = await scanOnce({ config, runtime, state });
|
|
@@ -14964,6 +15192,38 @@ async function main() {
|
|
|
14964
15192
|
console.error(`[scan-error] ${error.message}`);
|
|
14965
15193
|
}
|
|
14966
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
|
+
|
|
14967
15227
|
if (config.once) {
|
|
14968
15228
|
break;
|
|
14969
15229
|
}
|
|
@@ -14973,6 +15233,8 @@ async function main() {
|
|
|
14973
15233
|
} finally {
|
|
14974
15234
|
runtime.stopping = true;
|
|
14975
15235
|
|
|
15236
|
+
stopRelayPolling();
|
|
15237
|
+
|
|
14976
15238
|
if (runtime.ipcClient) {
|
|
14977
15239
|
runtime.ipcClient.stop();
|
|
14978
15240
|
}
|
package/scripts/viveworker.mjs
CHANGED
|
@@ -42,6 +42,17 @@ if (rawArgs[0] === "moltbook") {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
if (rawArgs[0] === "a2a") {
|
|
46
|
+
const { runA2ACli } = await import("./a2a-cli.mjs");
|
|
47
|
+
try {
|
|
48
|
+
await runA2ACli(rawArgs.slice(1));
|
|
49
|
+
process.exit(0);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(error.message || String(error));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
const cli = parseArgs(rawArgs);
|
|
46
57
|
|
|
47
58
|
try {
|
package/web/app.css
CHANGED
|
@@ -2318,6 +2318,25 @@ code {
|
|
|
2318
2318
|
border-color: var(--accent, #6ea8ff);
|
|
2319
2319
|
}
|
|
2320
2320
|
|
|
2321
|
+
.compose-greeting {
|
|
2322
|
+
display: flex;
|
|
2323
|
+
align-items: center;
|
|
2324
|
+
gap: 0.55rem;
|
|
2325
|
+
margin: 0.6rem 0 0.4rem;
|
|
2326
|
+
padding: 0.65rem 0.85rem;
|
|
2327
|
+
border-radius: 12px;
|
|
2328
|
+
border-left: 3px solid var(--completion, #d2aa63);
|
|
2329
|
+
background: rgba(210, 170, 99, 0.10);
|
|
2330
|
+
}
|
|
2331
|
+
.compose-greeting__icon {
|
|
2332
|
+
font-size: 1.25rem;
|
|
2333
|
+
line-height: 1;
|
|
2334
|
+
}
|
|
2335
|
+
.compose-greeting__text {
|
|
2336
|
+
font-size: 0.92rem;
|
|
2337
|
+
color: var(--text);
|
|
2338
|
+
font-weight: 500;
|
|
2339
|
+
}
|
|
2321
2340
|
.reply-field--title {
|
|
2322
2341
|
margin-bottom: 0.5rem;
|
|
2323
2342
|
}
|
|
@@ -3421,6 +3440,10 @@ button[data-action-url][aria-busy="true"]:not(.is-loading) {
|
|
|
3421
3440
|
color: #e07a5f;
|
|
3422
3441
|
background: rgba(224, 122, 95, 0.12);
|
|
3423
3442
|
}
|
|
3443
|
+
.provider-badge--a2a {
|
|
3444
|
+
color: #81c784;
|
|
3445
|
+
background: rgba(129, 199, 132, 0.12);
|
|
3446
|
+
}
|
|
3424
3447
|
|
|
3425
3448
|
.provider-filter {
|
|
3426
3449
|
display: inline-flex;
|
|
@@ -3431,18 +3454,22 @@ button[data-action-url][aria-busy="true"]:not(.is-loading) {
|
|
|
3431
3454
|
background: rgba(255, 255, 255, 0.04);
|
|
3432
3455
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
3433
3456
|
border-radius: 999px;
|
|
3434
|
-
width:
|
|
3457
|
+
max-width: 100%;
|
|
3458
|
+
overflow-x: auto;
|
|
3459
|
+
-webkit-overflow-scrolling: touch;
|
|
3435
3460
|
}
|
|
3436
3461
|
.provider-filter__button {
|
|
3437
3462
|
appearance: none;
|
|
3438
3463
|
background: transparent;
|
|
3439
3464
|
color: var(--text-muted, #9aa6b2);
|
|
3440
3465
|
border: 0;
|
|
3441
|
-
padding: 0.3rem 0.
|
|
3466
|
+
padding: 0.3rem 0.65rem;
|
|
3442
3467
|
font-size: 0.78rem;
|
|
3443
3468
|
font-weight: 600;
|
|
3444
3469
|
border-radius: 999px;
|
|
3445
3470
|
cursor: pointer;
|
|
3471
|
+
white-space: nowrap;
|
|
3472
|
+
flex-shrink: 0;
|
|
3446
3473
|
transition: background 0.15s ease, color 0.15s ease;
|
|
3447
3474
|
}
|
|
3448
3475
|
.provider-filter__button:hover {
|