viveworker 0.8.0 → 0.8.1
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +82 -0
- package/package.json +7 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
- package/scripts/lib/markdown-render.mjs +128 -1
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +350 -22
- package/scripts/viveworker.mjs +262 -1
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +49 -0
- package/web/app.js +51 -2
- package/web/build-id.js +1 -0
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +1 -1
- package/web/sw.js +2 -2
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
|
@@ -16,6 +16,7 @@ import zlib from "node:zlib";
|
|
|
16
16
|
import webPush from "web-push";
|
|
17
17
|
import * as hazbaseAuth from "@hazbase/auth";
|
|
18
18
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
19
|
+
import { APP_BUILD_ID as WEB_APP_BUILD_ID } from "../web/build-id.js";
|
|
19
20
|
import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
|
|
20
21
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
21
22
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
@@ -48,8 +49,8 @@ const listSupportedPayments = typeof hazbaseAuth.listSupportedPayments === "func
|
|
|
48
49
|
? hazbaseAuth.listSupportedPayments.bind(hazbaseAuth)
|
|
49
50
|
: async () => ({ networks: [], defaultNetwork: "base-sepolia" });
|
|
50
51
|
const appPackageVersion = readPackageVersion();
|
|
51
|
-
const WEB_APP_BUILD_ID = "20260428-remote-token-refresh";
|
|
52
52
|
const WEB_APP_SCRIPT_URL = `/app.js?v=${WEB_APP_BUILD_ID}`;
|
|
53
|
+
const WEB_APP_BUILD_ID_PLACEHOLDER = "__VIVEWORKER_APP_BUILD_ID__";
|
|
53
54
|
const sessionCookieName = "viveworker_session";
|
|
54
55
|
const deviceCookieName = "viveworker_device";
|
|
55
56
|
const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "approval", "plan", "choice", "info", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
@@ -2839,6 +2840,7 @@ function normalizeProvider(value) {
|
|
|
2839
2840
|
if (normalized === "moltbook") return "moltbook";
|
|
2840
2841
|
if (normalized === "a2a") return "a2a";
|
|
2841
2842
|
if (normalized === "viveworker") return "viveworker";
|
|
2843
|
+
if (normalized === "mcp") return "mcp";
|
|
2842
2844
|
return "codex";
|
|
2843
2845
|
}
|
|
2844
2846
|
|
|
@@ -2848,6 +2850,7 @@ function providerDisplayName(locale, provider) {
|
|
|
2848
2850
|
if (p === "moltbook") return "Moltbook";
|
|
2849
2851
|
if (p === "a2a") return "A2A";
|
|
2850
2852
|
if (p === "viveworker") return t(locale, "common.appName");
|
|
2853
|
+
if (p === "mcp") return "MCP";
|
|
2851
2854
|
return t(locale, "common.codex");
|
|
2852
2855
|
}
|
|
2853
2856
|
|
|
@@ -2896,7 +2899,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2896
2899
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
2897
2900
|
const rawThreadLabel = cleanText(raw.threadLabel ?? "");
|
|
2898
2901
|
const historyProvider = normalizeProvider(raw.provider);
|
|
2899
|
-
const skipHistoryThreadLabelRewrite = historyProvider === "moltbook" || historyProvider === "a2a" || historyProvider === "viveworker"
|
|
2902
|
+
const skipHistoryThreadLabelRewrite = historyProvider === "moltbook" || historyProvider === "a2a" || historyProvider === "viveworker" || historyProvider === "mcp"
|
|
2900
2903
|
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
2901
2904
|
const threadLabel = skipHistoryThreadLabelRewrite
|
|
2902
2905
|
? rawThreadLabel
|
|
@@ -3183,7 +3186,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
3183
3186
|
(kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
|
|
3184
3187
|
"";
|
|
3185
3188
|
const rawProvider = normalizeProvider(raw.provider);
|
|
3186
|
-
const skipThreadLabelRewrite = rawProvider === "moltbook" || rawProvider === "a2a" || rawProvider === "viveworker"
|
|
3189
|
+
const skipThreadLabelRewrite = rawProvider === "moltbook" || rawProvider === "a2a" || rawProvider === "viveworker" || rawProvider === "mcp"
|
|
3187
3190
|
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
3188
3191
|
const threadLabel =
|
|
3189
3192
|
skipThreadLabelRewrite
|
|
@@ -4354,7 +4357,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4354
4357
|
if (block?.type === "text" && block.text) text += block.text;
|
|
4355
4358
|
}
|
|
4356
4359
|
}
|
|
4357
|
-
|
|
4360
|
+
// Preserve paragraph structure for the renderer — `cleanText` collapses
|
|
4361
|
+
// every \s+ (newlines included) into a single space, which would flatten
|
|
4362
|
+
// markdown tables / code fences / lists into a single illegible line.
|
|
4363
|
+
text = normalizeLongText(text);
|
|
4358
4364
|
if (!text) continue;
|
|
4359
4365
|
if (type === "user" && record.entrypoint === "sdk-cli") {
|
|
4360
4366
|
const derivedThreadLabel = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
@@ -10644,6 +10650,273 @@ function buildPushStatusResponse(config, state, session) {
|
|
|
10644
10650
|
};
|
|
10645
10651
|
}
|
|
10646
10652
|
|
|
10653
|
+
async function buildMcpStatusResponse(config, runtime, state) {
|
|
10654
|
+
const remote = getRemotePairingStatus(runtime);
|
|
10655
|
+
let remotePairings = 0;
|
|
10656
|
+
try {
|
|
10657
|
+
remotePairings = (await loadPairings()).length;
|
|
10658
|
+
} catch {
|
|
10659
|
+
remotePairings = 0;
|
|
10660
|
+
}
|
|
10661
|
+
const now = Date.now();
|
|
10662
|
+
const pairedDevices = isPlainObject(state.pairedDevices) ? state.pairedDevices : {};
|
|
10663
|
+
const trustedDevices = Object.values(pairedDevices).filter((raw) => {
|
|
10664
|
+
const record = normalizeDeviceTrustRecord(raw, { trustTtlMs: config.deviceTrustTtlMs, now });
|
|
10665
|
+
return record && !record.revokedAtMs && Number(record.trustedUntilMs) > now;
|
|
10666
|
+
}).length;
|
|
10667
|
+
const a2aRelay = getRelayStatus();
|
|
10668
|
+
return {
|
|
10669
|
+
ok: true,
|
|
10670
|
+
bridge: {
|
|
10671
|
+
running: true,
|
|
10672
|
+
publicBaseUrl: config.nativeApprovalPublicBaseUrl,
|
|
10673
|
+
locale: config.defaultLocale,
|
|
10674
|
+
codexConnected: Boolean(runtime.ipcClient?.clientId),
|
|
10675
|
+
},
|
|
10676
|
+
pairing: {
|
|
10677
|
+
trustedDevices,
|
|
10678
|
+
webPushEnabled: Boolean(config.webPushEnabled),
|
|
10679
|
+
pushSubscriptions: listPushSubscriptions(state).length,
|
|
10680
|
+
},
|
|
10681
|
+
remote: {
|
|
10682
|
+
enabled: Boolean(remote.enabled),
|
|
10683
|
+
relayUrl: remote.relayUrl || config.remotePairingRelayUrl || "",
|
|
10684
|
+
identityFingerprint: remote.identityFingerprint || null,
|
|
10685
|
+
pairings: remotePairings,
|
|
10686
|
+
sessions: Array.isArray(remote.sessions)
|
|
10687
|
+
? remote.sessions.map((session) => ({
|
|
10688
|
+
state: cleanText(session?.state || ""),
|
|
10689
|
+
lastRoute: cleanText(session?.lastRoute || ""),
|
|
10690
|
+
connected: cleanText(session?.state || "") === "connected",
|
|
10691
|
+
phoneFingerprint: cleanText(session?.phoneFingerprint || ""),
|
|
10692
|
+
lastSeenAtMs: Number(session?.lastSeenAtMs) || 0,
|
|
10693
|
+
}))
|
|
10694
|
+
: [],
|
|
10695
|
+
},
|
|
10696
|
+
a2a: {
|
|
10697
|
+
enabled: Boolean(config.a2aApiKey),
|
|
10698
|
+
relayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
10699
|
+
relayConnected: Boolean(a2aRelay.polling && a2aRelay.lastPollOk),
|
|
10700
|
+
userIdConfigured: Boolean(config.a2aRelayUserId),
|
|
10701
|
+
},
|
|
10702
|
+
share: {
|
|
10703
|
+
enabled: Boolean(config.a2aRelayUserId && config.a2aApiKey),
|
|
10704
|
+
shareUrl: config.a2aShareUrl || "",
|
|
10705
|
+
},
|
|
10706
|
+
moltbook: {
|
|
10707
|
+
enabled: Boolean(config.moltbookApiKey),
|
|
10708
|
+
},
|
|
10709
|
+
};
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10712
|
+
function mcpTimeoutMs(value) {
|
|
10713
|
+
const n = Number(value);
|
|
10714
|
+
if (!Number.isFinite(n)) return 600_000;
|
|
10715
|
+
return Math.max(10_000, Math.min(900_000, Math.floor(n)));
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10718
|
+
function mcpText(value, maxChars = 50_000) {
|
|
10719
|
+
const text = String(value ?? "");
|
|
10720
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}\n\n[truncated by viveworker MCP]` : text;
|
|
10721
|
+
}
|
|
10722
|
+
|
|
10723
|
+
function normalizeMcpStringArray(value, maxItems = 20) {
|
|
10724
|
+
if (!Array.isArray(value)) return [];
|
|
10725
|
+
return value
|
|
10726
|
+
.map((item) => cleanText(item ?? ""))
|
|
10727
|
+
.filter(Boolean)
|
|
10728
|
+
.slice(0, maxItems);
|
|
10729
|
+
}
|
|
10730
|
+
|
|
10731
|
+
function normalizeMcpQuestionOptions(value) {
|
|
10732
|
+
if (!Array.isArray(value)) return [];
|
|
10733
|
+
return value
|
|
10734
|
+
.map((option, index) => {
|
|
10735
|
+
if (typeof option === "string") {
|
|
10736
|
+
const label = cleanText(option);
|
|
10737
|
+
return label ? { label } : null;
|
|
10738
|
+
}
|
|
10739
|
+
if (!isPlainObject(option)) return null;
|
|
10740
|
+
const label = cleanText(option.label ?? option.title ?? `Option ${index + 1}`);
|
|
10741
|
+
if (!label) return null;
|
|
10742
|
+
const description = cleanText(option.description ?? option.detail ?? "");
|
|
10743
|
+
return { label, ...(description ? { description } : {}) };
|
|
10744
|
+
})
|
|
10745
|
+
.filter(Boolean)
|
|
10746
|
+
.slice(0, 10);
|
|
10747
|
+
}
|
|
10748
|
+
|
|
10749
|
+
function normalizeMcpApprovalKind(value) {
|
|
10750
|
+
const normalized = cleanText(value || "mcp").toLowerCase();
|
|
10751
|
+
if (!/^[a-z0-9_-]{1,40}$/u.test(normalized)) {
|
|
10752
|
+
return "mcp";
|
|
10753
|
+
}
|
|
10754
|
+
// Keep MCP in the normal approval lane. These kinds have provider-specific
|
|
10755
|
+
// UI/actions elsewhere and should not be spoofable from a generic MCP client.
|
|
10756
|
+
if (normalized === "payment" || normalized === "hazbase_wallet_payment" || normalized === "plan" || normalized === "question") {
|
|
10757
|
+
return "mcp";
|
|
10758
|
+
}
|
|
10759
|
+
return normalized;
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
function createMcpPendingApproval(body, kind) {
|
|
10763
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
10764
|
+
const requestId = cleanText(body.requestId || crypto.randomUUID());
|
|
10765
|
+
const title = cleanText(body.title || (kind === "question" ? "MCP question" : "MCP approval"));
|
|
10766
|
+
const messageText = kind === "question"
|
|
10767
|
+
? mcpText(body.question || body.message || "")
|
|
10768
|
+
: mcpText(body.messageText || body.message || "");
|
|
10769
|
+
const requestKey = `mcp:${requestId}`;
|
|
10770
|
+
const approval = {
|
|
10771
|
+
token,
|
|
10772
|
+
requestKey,
|
|
10773
|
+
conversationId: "mcp",
|
|
10774
|
+
requestId,
|
|
10775
|
+
ownerClientId: null,
|
|
10776
|
+
kind,
|
|
10777
|
+
threadLabel: title || "MCP",
|
|
10778
|
+
title,
|
|
10779
|
+
messageText,
|
|
10780
|
+
fileRefs: normalizeMcpStringArray(body.fileRefs),
|
|
10781
|
+
previousFileRefs: [],
|
|
10782
|
+
diffText: kind === "question" ? "" : mcpText(body.diffText || "", 100_000),
|
|
10783
|
+
diffAvailable: kind !== "question" && Boolean(body.diffText),
|
|
10784
|
+
diffSource: kind === "question" ? "" : "mcp",
|
|
10785
|
+
diffAddedLines: 0,
|
|
10786
|
+
diffRemovedLines: 0,
|
|
10787
|
+
cwd: "",
|
|
10788
|
+
workspaceRoot: "",
|
|
10789
|
+
createdAtMs: Date.now(),
|
|
10790
|
+
resolved: false,
|
|
10791
|
+
resolving: false,
|
|
10792
|
+
resolveMcpWaiter: null,
|
|
10793
|
+
provider: "mcp",
|
|
10794
|
+
planText: "",
|
|
10795
|
+
questions: [],
|
|
10796
|
+
notifyOnly: false,
|
|
10797
|
+
readOnly: false,
|
|
10798
|
+
};
|
|
10799
|
+
if (kind === "question") {
|
|
10800
|
+
approval.questions = [
|
|
10801
|
+
{
|
|
10802
|
+
question: mcpText(body.question || body.message || "", 2000),
|
|
10803
|
+
options: normalizeMcpQuestionOptions(body.options),
|
|
10804
|
+
multiSelect: body.multiSelect === true,
|
|
10805
|
+
},
|
|
10806
|
+
];
|
|
10807
|
+
}
|
|
10808
|
+
return approval;
|
|
10809
|
+
}
|
|
10810
|
+
|
|
10811
|
+
async function waitForMcpApprovalResult(runtime, approval, timeoutMs) {
|
|
10812
|
+
const waitMs = mcpTimeoutMs(timeoutMs);
|
|
10813
|
+
const result = await new Promise((resolve) => {
|
|
10814
|
+
const timer = setTimeout(() => {
|
|
10815
|
+
resolve({ approved: false, decision: "timeout" });
|
|
10816
|
+
}, waitMs);
|
|
10817
|
+
approval.resolveMcpWaiter = (value) => {
|
|
10818
|
+
clearTimeout(timer);
|
|
10819
|
+
resolve(value);
|
|
10820
|
+
};
|
|
10821
|
+
});
|
|
10822
|
+
if (!approval.resolved) {
|
|
10823
|
+
approval.resolved = true;
|
|
10824
|
+
approval.resolving = false;
|
|
10825
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
10826
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
10827
|
+
}
|
|
10828
|
+
return result;
|
|
10829
|
+
}
|
|
10830
|
+
|
|
10831
|
+
async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
10832
|
+
const eventType = cleanText(body.eventType || "");
|
|
10833
|
+
if (eventType === "status") {
|
|
10834
|
+
return writeJson(res, 200, await buildMcpStatusResponse(config, runtime, state));
|
|
10835
|
+
}
|
|
10836
|
+
|
|
10837
|
+
if (eventType === "notify") {
|
|
10838
|
+
const title = cleanText(body.title || "MCP");
|
|
10839
|
+
const messageText = mcpText(body.message || body.messageText || "");
|
|
10840
|
+
if (!messageText) {
|
|
10841
|
+
return writeJson(res, 400, { error: "missing-message" });
|
|
10842
|
+
}
|
|
10843
|
+
const stableId = `mcp_notify:${historyToken(`${title}:${messageText}:${Date.now()}`)}`;
|
|
10844
|
+
const token = historyToken(stableId);
|
|
10845
|
+
const entry = {
|
|
10846
|
+
stableId,
|
|
10847
|
+
token,
|
|
10848
|
+
kind: "assistant_commentary",
|
|
10849
|
+
threadId: "mcp",
|
|
10850
|
+
threadLabel: cleanText(body.threadLabel || "MCP"),
|
|
10851
|
+
title,
|
|
10852
|
+
summary: formatNotificationBody(messageText, 160) || messageText,
|
|
10853
|
+
messageText,
|
|
10854
|
+
createdAtMs: Date.now(),
|
|
10855
|
+
readOnly: true,
|
|
10856
|
+
provider: "mcp",
|
|
10857
|
+
};
|
|
10858
|
+
const timelineChanged = recordTimelineEntry({ config, runtime, state, entry });
|
|
10859
|
+
const historyChanged = recordHistoryItem({ config, runtime, state, item: entry });
|
|
10860
|
+
const pushChanged = await deliverWebPushItem({
|
|
10861
|
+
config,
|
|
10862
|
+
state,
|
|
10863
|
+
kind: "assistant_commentary",
|
|
10864
|
+
tab: "timeline",
|
|
10865
|
+
token,
|
|
10866
|
+
stableId,
|
|
10867
|
+
title,
|
|
10868
|
+
body: messageText,
|
|
10869
|
+
}).catch((error) => {
|
|
10870
|
+
console.error(`[mcp-notify-push] ${error.message}`);
|
|
10871
|
+
return false;
|
|
10872
|
+
});
|
|
10873
|
+
if (timelineChanged || historyChanged || pushChanged) {
|
|
10874
|
+
await saveState(config.stateFile, state);
|
|
10875
|
+
}
|
|
10876
|
+
return writeJson(res, 200, { ok: true, token });
|
|
10877
|
+
}
|
|
10878
|
+
|
|
10879
|
+
if (eventType === "approval_request" || eventType === "choice_request") {
|
|
10880
|
+
const kind = eventType === "choice_request" ? "question" : normalizeMcpApprovalKind(body.approvalKind);
|
|
10881
|
+
if (kind !== "question" && !mcpText(body.message || body.messageText || "")) {
|
|
10882
|
+
return writeJson(res, 400, { error: "missing-message" });
|
|
10883
|
+
}
|
|
10884
|
+
if (kind === "question" && !mcpText(body.question || body.message || "")) {
|
|
10885
|
+
return writeJson(res, 400, { error: "missing-question" });
|
|
10886
|
+
}
|
|
10887
|
+
const approval = createMcpPendingApproval(body, kind);
|
|
10888
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
10889
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
10890
|
+
deliverWebPushItem({
|
|
10891
|
+
config,
|
|
10892
|
+
state,
|
|
10893
|
+
kind: "approval",
|
|
10894
|
+
tab: "inbox",
|
|
10895
|
+
subtab: "pending",
|
|
10896
|
+
token: approval.token,
|
|
10897
|
+
stableId: pendingApprovalStableId(approval),
|
|
10898
|
+
title: approval.title || "MCP",
|
|
10899
|
+
body: approval.messageText,
|
|
10900
|
+
buildLocalizedContent: () => ({
|
|
10901
|
+
title: approval.title || "MCP",
|
|
10902
|
+
body: approval.messageText,
|
|
10903
|
+
}),
|
|
10904
|
+
}).catch((error) => {
|
|
10905
|
+
console.error(`[mcp-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
10906
|
+
});
|
|
10907
|
+
const result = await waitForMcpApprovalResult(runtime, approval, body.timeoutMs);
|
|
10908
|
+
return writeJson(res, 200, {
|
|
10909
|
+
ok: true,
|
|
10910
|
+
approved: result?.approved === true || result?.decision === "accept",
|
|
10911
|
+
decision: result?.decision || (result?.approved ? "accept" : "reject"),
|
|
10912
|
+
...(result?.answerText ? { answerText: result.answerText } : {}),
|
|
10913
|
+
...(Array.isArray(result?.answers) ? { answers: result.answers } : {}),
|
|
10914
|
+
});
|
|
10915
|
+
}
|
|
10916
|
+
|
|
10917
|
+
return writeJson(res, 400, { error: "unsupported-mcp-event" });
|
|
10918
|
+
}
|
|
10919
|
+
|
|
10647
10920
|
function requestUserAgent(req) {
|
|
10648
10921
|
return cleanText(req.headers?.["user-agent"] ?? "");
|
|
10649
10922
|
}
|
|
@@ -13380,7 +13653,12 @@ async function autoApproveTrustedWrite({
|
|
|
13380
13653
|
}
|
|
13381
13654
|
|
|
13382
13655
|
async function handleNativeApprovalDecision({ config, runtime, state, approval, decision }) {
|
|
13383
|
-
if (approval.
|
|
13656
|
+
if (approval.resolveMcpWaiter) {
|
|
13657
|
+
approval.resolveMcpWaiter({
|
|
13658
|
+
approved: decision === "accept",
|
|
13659
|
+
decision,
|
|
13660
|
+
});
|
|
13661
|
+
} else if (approval.resolveClaudeWaiter) {
|
|
13384
13662
|
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
13385
13663
|
// ExitPlanMode cannot be truly auto-approved via permissionDecision: "allow"
|
|
13386
13664
|
// (Claude still shows the native PC plan dialog). Instead, deny the tool
|
|
@@ -13751,13 +14029,32 @@ const GZIPPABLE_EXTENSIONS = new Set([
|
|
|
13751
14029
|
]);
|
|
13752
14030
|
const MIN_GZIP_BYTES = 512;
|
|
13753
14031
|
|
|
14032
|
+
function renderWebAssetBuffer(filePath, raw) {
|
|
14033
|
+
const basename = path.basename(filePath);
|
|
14034
|
+
if (basename !== "index.html" && basename !== "sw.js" && basename !== "app.js") {
|
|
14035
|
+
return raw;
|
|
14036
|
+
}
|
|
14037
|
+
|
|
14038
|
+
const text = raw.toString("utf8");
|
|
14039
|
+
if (!text.includes(WEB_APP_BUILD_ID_PLACEHOLDER)) {
|
|
14040
|
+
return raw;
|
|
14041
|
+
}
|
|
14042
|
+
|
|
14043
|
+
return Buffer.from(text.replaceAll(WEB_APP_BUILD_ID_PLACEHOLDER, WEB_APP_BUILD_ID), "utf8");
|
|
14044
|
+
}
|
|
14045
|
+
|
|
13754
14046
|
async function loadWebAssetEntry(filePath) {
|
|
13755
14047
|
const stat = await fs.stat(filePath);
|
|
13756
14048
|
const cached = WEB_ASSET_CACHE.get(filePath);
|
|
13757
|
-
if (
|
|
14049
|
+
if (
|
|
14050
|
+
cached &&
|
|
14051
|
+
cached.mtimeMs === stat.mtimeMs &&
|
|
14052
|
+
cached.sourceSize === stat.size &&
|
|
14053
|
+
cached.buildId === WEB_APP_BUILD_ID
|
|
14054
|
+
) {
|
|
13758
14055
|
return cached;
|
|
13759
14056
|
}
|
|
13760
|
-
const raw = await fs.readFile(filePath);
|
|
14057
|
+
const raw = renderWebAssetBuffer(filePath, await fs.readFile(filePath));
|
|
13761
14058
|
const extension = path.extname(filePath).toLowerCase();
|
|
13762
14059
|
let gzip = null;
|
|
13763
14060
|
if (GZIPPABLE_EXTENSIONS.has(extension) && raw.length >= MIN_GZIP_BYTES) {
|
|
@@ -13771,7 +14068,8 @@ async function loadWebAssetEntry(filePath) {
|
|
|
13771
14068
|
raw,
|
|
13772
14069
|
gzip,
|
|
13773
14070
|
mtimeMs: stat.mtimeMs,
|
|
13774
|
-
|
|
14071
|
+
sourceSize: stat.size,
|
|
14072
|
+
buildId: WEB_APP_BUILD_ID,
|
|
13775
14073
|
contentType: contentTypeForFile(filePath),
|
|
13776
14074
|
};
|
|
13777
14075
|
WEB_ASSET_CACHE.set(filePath, entry);
|
|
@@ -14173,6 +14471,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
14173
14471
|
if (
|
|
14174
14472
|
url.pathname === "/app.js" ||
|
|
14175
14473
|
url.pathname === "/app.css" ||
|
|
14474
|
+
url.pathname === "/build-id.js" ||
|
|
14176
14475
|
url.pathname === "/i18n.js" ||
|
|
14177
14476
|
url.pathname === "/hazbase-passkey.js" ||
|
|
14178
14477
|
url.pathname === "/sw.js" ||
|
|
@@ -15748,7 +16047,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15748
16047
|
messageText: content,
|
|
15749
16048
|
createdAtMs: share.createdAtMs,
|
|
15750
16049
|
readOnly: false,
|
|
15751
|
-
provider: "viveworker",
|
|
16050
|
+
provider: sourceTool === "mcp" ? "mcp" : "viveworker",
|
|
15752
16051
|
},
|
|
15753
16052
|
});
|
|
15754
16053
|
await saveState(config.stateFile, state);
|
|
@@ -16252,6 +16551,22 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16252
16551
|
return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
|
|
16253
16552
|
}
|
|
16254
16553
|
|
|
16554
|
+
if (url.pathname === "/api/providers/mcp/events" && req.method === "POST") {
|
|
16555
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
16556
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
16557
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
16558
|
+
}
|
|
16559
|
+
|
|
16560
|
+
let body;
|
|
16561
|
+
try {
|
|
16562
|
+
body = await parseJsonBody(req);
|
|
16563
|
+
} catch {
|
|
16564
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
16565
|
+
}
|
|
16566
|
+
|
|
16567
|
+
return handleMcpProviderEvent({ config, runtime, state, body, res });
|
|
16568
|
+
}
|
|
16569
|
+
|
|
16255
16570
|
if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
|
|
16256
16571
|
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
16257
16572
|
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
@@ -17127,7 +17442,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17127
17442
|
|
|
17128
17443
|
approval.resolving = true;
|
|
17129
17444
|
try {
|
|
17130
|
-
if (approval.
|
|
17445
|
+
if (approval.resolveMcpWaiter) {
|
|
17446
|
+
approval.resolveMcpWaiter({
|
|
17447
|
+
approved: true,
|
|
17448
|
+
decision: "answered",
|
|
17449
|
+
answers,
|
|
17450
|
+
answerText: reasonText,
|
|
17451
|
+
});
|
|
17452
|
+
} else if (approval.resolveClaudeWaiter) {
|
|
17131
17453
|
approval.resolveClaudeWaiter({
|
|
17132
17454
|
permissionDecision: "deny",
|
|
17133
17455
|
permissionDecisionReason: reasonText,
|
|
@@ -17163,8 +17485,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17163
17485
|
if (stateChanged) {
|
|
17164
17486
|
await saveState(config.stateFile, state);
|
|
17165
17487
|
}
|
|
17166
|
-
console.log(`[claude-question-answer] ${approval.requestKey}`);
|
|
17167
|
-
|
|
17488
|
+
console.log(`[${approval.provider === "mcp" ? "mcp" : "claude"}-question-answer] ${approval.requestKey}`);
|
|
17489
|
+
if (approval.provider === "claude") {
|
|
17490
|
+
activateClaudeDesktopIfMac(req);
|
|
17491
|
+
}
|
|
17168
17492
|
return writeJson(res, 200, { ok: true });
|
|
17169
17493
|
} catch (error) {
|
|
17170
17494
|
approval.resolving = false;
|
|
@@ -20109,16 +20433,20 @@ function extractRolloutMessageText(content) {
|
|
|
20109
20433
|
if (!Array.isArray(content)) {
|
|
20110
20434
|
return "";
|
|
20111
20435
|
}
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20436
|
+
// The per-entry text is already passed through `normalizeTimelineMessageText`
|
|
20437
|
+
// (which preserves newlines for the markdown renderer). The final wrap used
|
|
20438
|
+
// to be `cleanText`, which collapses every \s+ — newlines included — into
|
|
20439
|
+
// a single space, flattening tables / code fences / lists into one
|
|
20440
|
+
// illegible line. Use a plain trim instead so the structure survives.
|
|
20441
|
+
return content
|
|
20442
|
+
.map((entry) =>
|
|
20443
|
+
isPlainObject(entry) && (entry.type === "input_text" || entry.type === "output_text")
|
|
20444
|
+
? normalizeTimelineMessageText(entry.text ?? "")
|
|
20445
|
+
: ""
|
|
20446
|
+
)
|
|
20447
|
+
.filter(Boolean)
|
|
20448
|
+
.join("\n")
|
|
20449
|
+
.trim();
|
|
20122
20450
|
}
|
|
20123
20451
|
|
|
20124
20452
|
function extractTitleOnlyJsonTitleFromRolloutContent(content) {
|