chore: Run pnpm format:fix.

This commit is contained in:
cpojer
2026-01-31 21:13:13 +09:00
parent dcc2de15a6
commit 8cab78abbc
624 changed files with 10729 additions and 7514 deletions

View File

@@ -1,8 +1,6 @@
{
"id": "bluebubbles",
"channels": [
"bluebubbles"
],
"channels": ["bluebubbles"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -300,9 +300,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
const resolvedChatGuid = await resolveChatGuid();
const base64Buffer = readStringParam(params, "buffer");
const filename =
readStringParam(params, "filename") ??
readStringParam(params, "name") ??
"icon.png";
readStringParam(params, "filename") ?? readStringParam(params, "name") ?? "icon.png";
const contentType =
readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");

View File

@@ -39,8 +39,10 @@ function ensureExtension(filename: string, extension: string, fallbackBase: stri
function resolveVoiceInfo(filename: string, contentType?: string) {
const normalizedType = contentType?.trim().toLowerCase();
const extension = path.extname(filename).toLowerCase();
const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
const isCaf = extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
const isMp3 =
extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
const isCaf =
extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
const isAudio = isMp3 || isCaf || Boolean(normalizedType?.startsWith("audio/"));
return { isAudio, isMp3, isCaf };
}
@@ -110,7 +112,10 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget {
function extractMessageId(payload: unknown): string {
if (!payload || typeof payload !== "object") return "unknown";
const record = payload as Record<string, unknown>;
const data = record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
const data =
record.data && typeof record.data === "object"
? (record.data as Record<string, unknown>)
: null;
const candidates = [
record.messageId,
record.guid,
@@ -205,9 +210,7 @@ export async function sendBlueBubblesAttachment(params: {
const addFile = (name: string, fileBuffer: Uint8Array, fileName: string, mimeType?: string) => {
parts.push(encoder.encode(`--${boundary}\r\n`));
parts.push(
encoder.encode(
`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`,
),
encoder.encode(`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`),
);
parts.push(encoder.encode(`Content-Type: ${mimeType ?? "application/octet-stream"}\r\n\r\n`));
parts.push(fileBuffer);
@@ -229,10 +232,7 @@ export async function sendBlueBubblesAttachment(params: {
const trimmedReplyTo = replyToMessageGuid?.trim();
if (trimmedReplyTo) {
addField("selectedMessageGuid", trimmedReplyTo);
addField(
"partIndex",
typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0",
);
addField("partIndex", typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0");
}
// Add optional caption
@@ -268,7 +268,9 @@ export async function sendBlueBubblesAttachment(params: {
if (!res.ok) {
const errorText = await res.text();
throw new Error(`BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`);
throw new Error(
`BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`,
);
}
const responseBody = await res.text();

View File

@@ -106,10 +106,9 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
baseUrl: account.baseUrl,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ??
[]).map(
(entry) => String(entry),
),
(
resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? []
).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())

View File

@@ -249,7 +249,9 @@ export async function removeBlueBubblesParticipant(
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`);
throw new Error(
`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`,
);
}
}
@@ -270,11 +272,7 @@ export async function leaveBlueBubblesChat(
password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "POST" },
opts.timeoutMs,
);
const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
@@ -313,9 +311,7 @@ export async function setGroupIconBlueBubbles(
// Add file field named "icon" as per API spec
parts.push(encoder.encode(`--${boundary}\r\n`));
parts.push(
encoder.encode(
`Content-Disposition: form-data; name="icon"; filename="${filename}"\r\n`,
),
encoder.encode(`Content-Disposition: form-data; name="icon"; filename="${filename}"\r\n`),
);
parts.push(
encoder.encode(`Content-Type: ${opts.contentType ?? "application/octet-stream"}\r\n\r\n`),

View File

@@ -78,88 +78,131 @@ function createMockRuntime(): PluginRuntime {
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
},
system: {
enqueueSystemEvent: mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
enqueueSystemEvent:
mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
},
media: {
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
isVoiceCompatibleAudio: vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
isVoiceCompatibleAudio:
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
},
tools: {
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
createMemorySearchTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
createMemorySearchTool:
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
},
channel: {
text: {
chunkMarkdownText: mockChunkMarkdownText as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownText"],
chunkMarkdownText:
mockChunkMarkdownText as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownText"],
chunkText: vi.fn() as unknown as PluginRuntime["channel"]["text"]["chunkText"],
resolveTextChunkLimit: vi.fn(() => 4000) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
hasControlCommand: mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
resolveMarkdownTableMode: vi.fn(() => "code") as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
convertMarkdownTables: vi.fn((text: string) => text) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
resolveTextChunkLimit: vi.fn(
() => 4000,
) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
hasControlCommand:
mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
resolveMarkdownTableMode: vi.fn(
() => "code",
) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
convertMarkdownTables: vi.fn(
(text: string) => text,
) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
},
reply: {
dispatchReplyWithBufferedBlockDispatcher: mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
createReplyDispatcherWithTyping: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
resolveEffectiveMessagesConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
resolveHumanDelayConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
dispatchReplyFromConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
finalizeInboundContext: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
formatAgentEnvelope: mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
formatInboundEnvelope: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
resolveEnvelopeFormatOptions: mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
dispatchReplyWithBufferedBlockDispatcher:
mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
createReplyDispatcherWithTyping:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
resolveEffectiveMessagesConfig:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
resolveHumanDelayConfig:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
dispatchReplyFromConfig:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
finalizeInboundContext:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
formatAgentEnvelope:
mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
formatInboundEnvelope:
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
resolveEnvelopeFormatOptions:
mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
},
routing: {
resolveAgentRoute: mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
resolveAgentRoute:
mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
},
pairing: {
buildPairingReply: mockBuildPairingReply as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
readAllowFromStore: mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
upsertPairingRequest: mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
buildPairingReply:
mockBuildPairingReply as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
readAllowFromStore:
mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
upsertPairingRequest:
mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
},
media: {
fetchRemoteMedia: vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
saveMediaBuffer: mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
fetchRemoteMedia:
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
saveMediaBuffer:
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
},
session: {
resolveStorePath: mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
readSessionUpdatedAt: mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
recordInboundSession: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
recordSessionMetaFromInbound: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
updateLastRoute: vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
resolveStorePath:
mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
readSessionUpdatedAt:
mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
recordInboundSession:
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
recordSessionMetaFromInbound:
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
updateLastRoute:
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
},
mentions: {
buildMentionRegexes: mockBuildMentionRegexes as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
matchesMentionPatterns: mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
buildMentionRegexes:
mockBuildMentionRegexes as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
matchesMentionPatterns:
mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
},
reactions: {
shouldAckReaction,
removeAckReactionAfterReply,
},
groups: {
resolveGroupPolicy: mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
resolveGroupPolicy:
mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
resolveRequireMention:
mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
},
debounce: {
// Create a pass-through debouncer that immediately calls onFlush
createInboundDebouncer: vi.fn((params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
enqueue: async (item: unknown) => {
await params.onFlush([item]);
},
flushKey: vi.fn(),
})) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
resolveInboundDebounceMs: vi.fn(() => 0) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
createInboundDebouncer: vi.fn(
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
enqueue: async (item: unknown) => {
await params.onFlush([item]);
},
flushKey: vi.fn(),
}),
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
resolveInboundDebounceMs: vi.fn(
() => 0,
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
},
commands: {
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
isControlCommandMessage: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
shouldComputeCommandAuthorized: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
shouldHandleTextCommands: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
resolveCommandAuthorizedFromAuthorizers:
mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
isControlCommandMessage:
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
shouldComputeCommandAuthorized:
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
shouldHandleTextCommands:
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
},
discord: {} as PluginRuntime["channel"]["discord"],
slack: {} as PluginRuntime["channel"]["slack"],
@@ -169,7 +212,9 @@ function createMockRuntime(): PluginRuntime {
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
},
logging: {
shouldLogVerbose: vi.fn(() => false) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
shouldLogVerbose: vi.fn(
() => false,
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
getChildLogger: vi.fn(() => ({
info: vi.fn(),
warn: vi.fn(),
@@ -178,12 +223,16 @@ function createMockRuntime(): PluginRuntime {
})) as unknown as PluginRuntime["logging"]["getChildLogger"],
},
state: {
resolveStateDir: vi.fn(() => "/tmp/openclaw") as unknown as PluginRuntime["state"]["resolveStateDir"],
resolveStateDir: vi.fn(
() => "/tmp/openclaw",
) as unknown as PluginRuntime["state"]["resolveStateDir"],
},
};
}
function createMockAccount(overrides: Partial<ResolvedBlueBubblesAccount["config"]> = {}): ResolvedBlueBubblesAccount {
function createMockAccount(
overrides: Partial<ResolvedBlueBubblesAccount["config"]> = {},
): ResolvedBlueBubblesAccount {
return {
accountId: "default",
enabled: true,
@@ -361,7 +410,9 @@ describe("BlueBubbles webhook monitor", () => {
guid: "msg-1",
},
});
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
@@ -399,7 +450,9 @@ describe("BlueBubbles webhook monitor", () => {
},
{ "x-password": "secret-token" },
);
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
@@ -432,7 +485,9 @@ describe("BlueBubbles webhook monitor", () => {
guid: "msg-1",
},
});
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
@@ -466,7 +521,9 @@ describe("BlueBubbles webhook monitor", () => {
},
});
// Localhost address
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "127.0.0.1" };
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "127.0.0.1",
};
unregister = registerBlueBubblesWebhookTarget({
account,
@@ -1161,7 +1218,10 @@ describe("BlueBubbles webhook monitor", () => {
// Use a timing-aware debouncer test double that respects debounceMs/buildKey/shouldDebounce.
core.channel.debounce.createInboundDebouncer = vi.fn((params: any) => {
type Item = any;
const buckets = new Map<string, { items: Item[]; timer: ReturnType<typeof setTimeout> | null }>();
const buckets = new Map<
string,
{ items: Item[]; timer: ReturnType<typeof setTimeout> | null }
>();
const flush = async (key: string) => {
const bucket = buckets.get(key);
@@ -2231,9 +2291,9 @@ describe("BlueBubbles webhook monitor", () => {
});
it("throws when numeric short ID is missing and requireKnownShortId is set", () => {
expect(() =>
resolveBlueBubblesMessageId("999", { requireKnownShortId: true }),
).toThrow(/short message id/i);
expect(() => resolveBlueBubblesMessageId("999", { requireKnownShortId: true })).toThrow(
/short message id/i,
);
});
});

View File

@@ -11,7 +11,11 @@ import {
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
import { downloadBlueBubblesAttachment } from "./attachments.js";
import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender, normalizeBlueBubblesHandle } from "./targets.js";
import {
formatBlueBubblesChatTarget,
isAllowedBlueBubblesSender,
normalizeBlueBubblesHandle,
} from "./targets.js";
import { sendBlueBubblesMedia } from "./media-send.js";
import type { BlueBubblesAccountConfig, BlueBubblesAttachment } from "./types.js";
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
@@ -194,7 +198,12 @@ function resolveReplyContextFromCache(params: {
// Avoid cross-chat collisions if we have identifiers.
if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) return null;
if (!chatGuid && chatIdentifier && cachedChatIdentifier && chatIdentifier !== cachedChatIdentifier) {
if (
!chatGuid &&
chatIdentifier &&
cachedChatIdentifier &&
chatIdentifier !== cachedChatIdentifier
) {
return null;
}
if (!chatGuid && !chatIdentifier && chatId && cachedChatId && chatId !== cachedChatId) {
@@ -206,7 +215,11 @@ function resolveReplyContextFromCache(params: {
type BlueBubblesCoreRuntime = ReturnType<typeof getBlueBubblesRuntime>;
function logVerbose(core: BlueBubblesCoreRuntime, runtime: BlueBubblesRuntimeEnv, message: string): void {
function logVerbose(
core: BlueBubblesCoreRuntime,
runtime: BlueBubblesRuntimeEnv,
message: string,
): void {
if (core.logging.shouldLogVerbose()) {
runtime.log?.(`[bluebubbles] ${message}`);
}
@@ -284,7 +297,7 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized
// Combine text from all entries, filtering out duplicates and empty strings
const seenTexts = new Set<string>();
const textParts: string[] = [];
for (const entry of entries) {
const text = entry.message.text.trim();
if (!text) continue;
@@ -398,10 +411,10 @@ function getOrCreateDebouncer(target: WebhookTarget) {
},
onFlush: async (entries) => {
if (entries.length === 0) return;
// Use target from first entry (all entries have same target due to key structure)
const flushTarget = entries[0].target;
if (entries.length === 1) {
// Single message - process normally
await processMessage(entries[0].message, flushTarget);
@@ -410,7 +423,7 @@ function getOrCreateDebouncer(target: WebhookTarget) {
// Multiple messages - combine and process
const combined = combineDebounceEntries(entries);
if (core.logging.shouldLogVerbose()) {
const count = entries.length;
const preview = combined.text.slice(0, 50);
@@ -418,7 +431,7 @@ function getOrCreateDebouncer(target: WebhookTarget) {
`[bluebubbles] coalesced ${count} messages: "${preview}${combined.text.length > 50 ? "..." : ""}"`,
);
}
await processMessage(combined, flushTarget);
},
onError: (err) => {
@@ -578,10 +591,7 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
}
// Returns inline reply tag like "[[reply_to:4]]" for prepending to message body
function formatReplyTag(message: {
replyToId?: string;
replyToShortId?: string;
}): string | null {
function formatReplyTag(message: { replyToId?: string; replyToShortId?: string }): string | null {
// Prefer short ID
const rawId = message.replyToShortId || message.replyToId;
if (!rawId) return null;
@@ -614,7 +624,8 @@ function extractReplyMetadata(message: Record<string, unknown>): {
message["associatedMessage"] ??
message["reply"];
const replyRecord = asRecord(replyRaw);
const replyHandle = asRecord(replyRecord?.["handle"]) ?? asRecord(replyRecord?.["sender"]) ?? null;
const replyHandle =
asRecord(replyRecord?.["handle"]) ?? asRecord(replyRecord?.["sender"]) ?? null;
const replySenderRaw =
readString(replyHandle, "address") ??
readString(replyHandle, "handle") ??
@@ -742,9 +753,7 @@ function formatGroupMembers(params: {
ordered.push(params.fallback);
}
if (ordered.length === 0) return undefined;
return ordered
.map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id))
.join(", ");
return ordered.map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id)).join(", ");
}
function resolveGroupFlagFromChatGuid(chatGuid?: string | null): boolean | undefined {
@@ -992,7 +1001,9 @@ function extractMessagePayload(payload: Record<string, unknown>): Record<string,
return message;
}
function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWebhookMessage | null {
function normalizeWebhookMessage(
payload: Record<string, unknown>,
): NormalizedWebhookMessage | null {
const message = extractMessagePayload(payload);
if (!message) return null;
@@ -1004,8 +1015,7 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
const handleValue = message.handle ?? message.sender;
const handle =
asRecord(handleValue) ??
(typeof handleValue === "string" ? { address: handleValue } : null);
asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
const senderId =
readString(handle, "address") ??
readString(handle, "handle") ??
@@ -1080,7 +1090,7 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
const isGroup =
typeof groupFromChatGuid === "boolean"
? groupFromChatGuid
: explicitIsGroup ?? (participantsCount > 2 ? true : false);
: (explicitIsGroup ?? (participantsCount > 2 ? true : false));
const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
const messageId =
@@ -1149,7 +1159,9 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
};
}
function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedWebhookReaction | null {
function normalizeWebhookReaction(
payload: Record<string, unknown>,
): NormalizedWebhookReaction | null {
const message = extractMessagePayload(payload);
if (!message) return null;
@@ -1173,8 +1185,7 @@ function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedW
const handleValue = message.handle ?? message.sender;
const handle =
asRecord(handleValue) ??
(typeof handleValue === "string" ? { address: handleValue } : null);
asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
const senderId =
readString(handle, "address") ??
readString(handle, "handle") ??
@@ -1247,7 +1258,7 @@ function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedW
const isGroup =
typeof groupFromChatGuid === "boolean"
? groupFromChatGuid
: explicitIsGroup ?? (participantsCount > 2 ? true : false);
: (explicitIsGroup ?? (participantsCount > 2 ? true : false));
const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
const timestampRaw =
@@ -1364,8 +1375,7 @@ export async function handleBlueBubblesWebhookRequest(
req.headers["x-password"] ??
req.headers["x-bluebubbles-guid"] ??
req.headers["authorization"];
const guid =
(Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
const guid = (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
if (guid && guid.trim() === token) return true;
const remote = req.socket?.remoteAddress ?? "";
if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") {
@@ -1630,7 +1640,7 @@ async function processMessage(
const chatGuid = message.chatGuid ?? undefined;
const chatIdentifier = message.chatIdentifier ?? undefined;
const peerId = isGroup
? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
: message.senderId;
const route = core.channel.routing.resolveAgentRoute({
@@ -1705,11 +1715,7 @@ async function processMessage(
// Allow control commands to bypass mention gating when authorized (parity with iMessage)
const shouldBypassMention =
isGroup &&
requireMention &&
!wasMentioned &&
commandAuthorized &&
hasControlCmd;
isGroup && requireMention && !wasMentioned && commandAuthorized && hasControlCmd;
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
// Skip group messages that require mention but weren't mentioned
@@ -1872,16 +1878,16 @@ async function processMessage(
const shouldAckReaction = () =>
Boolean(
ackReactionValue &&
core.channel.reactions.shouldAckReaction({
scope: ackReactionScope,
isDirect: !isGroup,
isGroup,
isMentionableGroup: isGroup,
requireMention: Boolean(requireMention),
canDetectMention,
effectiveWasMentioned,
shouldBypassMention,
}),
core.channel.reactions.shouldAckReaction({
scope: ackReactionScope,
isDirect: !isGroup,
isGroup,
isMentionableGroup: isGroup,
requireMention: Boolean(requireMention),
canDetectMention,
effectiveWasMentioned,
shouldBypassMention,
}),
);
const ackMessageId = message.messageId?.trim() || "";
const ackReactionPromise =
@@ -2000,7 +2006,8 @@ async function processMessage(
cfg: config,
dispatcherOptions: {
deliver: async (payload) => {
const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
const rawReplyToId =
typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
// Resolve short ID (e.g., "5") to full UUID
const replyToMessageGuid = rawReplyToId
? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
@@ -2210,7 +2217,7 @@ async function processReaction(
const chatGuid = reaction.chatGuid ?? undefined;
const chatIdentifier = reaction.chatIdentifier ?? undefined;
const peerId = reaction.isGroup
? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
: reaction.senderId;
const route = core.channel.routing.resolveAgentRoute({

View File

@@ -108,11 +108,7 @@ export async function probeBlueBubbles(params: {
if (!password) return { ok: false, error: "password not configured" };
const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/ping", password });
try {
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "GET" },
params.timeoutMs,
);
const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs);
if (!res.ok) {
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
}

View File

@@ -10,14 +10,7 @@ export type BlueBubblesReactionOpts = {
cfg?: OpenClawConfig;
};
const REACTION_TYPES = new Set([
"love",
"like",
"dislike",
"laugh",
"emphasize",
"question",
]);
const REACTION_TYPES = new Set(["love", "like", "dislike", "laugh", "emphasize", "question"]);
const REACTION_ALIASES = new Map<string, string>([
// General

View File

@@ -87,7 +87,9 @@ function extractMessageId(payload: unknown): string {
if (!payload || typeof payload !== "object") return "unknown";
const record = payload as Record<string, unknown>;
const data =
record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
record.data && typeof record.data === "object"
? (record.data as Record<string, unknown>)
: null;
const candidates = [
record.messageId,
record.messageGuid,
@@ -308,7 +310,11 @@ async function createNewChatWithMessage(params: {
if (!res.ok) {
const errorText = await res.text();
// Check for Private API not enabled error
if (res.status === 400 || res.status === 403 || errorText.toLowerCase().includes("private api")) {
if (
res.status === 400 ||
res.status === 403 ||
errorText.toLowerCase().includes("private api")
) {
throw new Error(
`BlueBubbles send failed: Cannot create new chat - Private API must be enabled. Original error: ${errorText || res.status}`,
);

View File

@@ -20,8 +20,7 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> =
{ prefix: "sms:", service: "sms" },
{ prefix: "auto:", service: "auto" },
];
const CHAT_IDENTIFIER_UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i;
function parseRawChatGuid(value: string): string | null {

View File

@@ -1,8 +1,6 @@
{
"id": "copilot-proxy",
"providers": [
"copilot-proxy"
],
"providers": ["copilot-proxy"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -1,13 +1,8 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"openclaw": {
"extensions": [
"./index.ts"
]
},
"type": "module",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
@@ -20,5 +15,10 @@
"@opentelemetry/sdk-node": "^0.211.0",
"@opentelemetry/sdk-trace-base": "^2.5.0",
"@opentelemetry/semantic-conventions": "^1.39.0"
},
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -192,13 +192,19 @@ describe("diagnostics-otel service", () => {
});
expect(telemetryState.counters.get("openclaw.webhook.received")?.add).toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.webhook.duration_ms")?.record).toHaveBeenCalled();
expect(
telemetryState.histograms.get("openclaw.webhook.duration_ms")?.record,
).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.message.queued")?.add).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.message.processed")?.add).toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.message.duration_ms")?.record).toHaveBeenCalled();
expect(
telemetryState.histograms.get("openclaw.message.duration_ms")?.record,
).toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.queue.wait_ms")?.record).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.session.stuck")?.add).toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.session.stuck_age_ms")?.record).toHaveBeenCalled();
expect(
telemetryState.histograms.get("openclaw.session.stuck_age_ms")?.record,
).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.run.attempt")?.add).toHaveBeenCalled();
const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0]);
@@ -209,7 +215,7 @@ describe("diagnostics-otel service", () => {
expect(registerLogTransportMock).toHaveBeenCalledTimes(1);
expect(registeredTransports).toHaveLength(1);
registeredTransports[0]?.({
0: "{\"subsystem\":\"diagnostic\"}",
0: '{"subsystem":"diagnostic"}',
1: "hello",
_meta: { logLevelName: "INFO", date: new Date() },
});

View File

@@ -273,7 +273,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
}
if (bindings) {
for (const [key, value] of Object.entries(bindings)) {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
attributes[`openclaw.${key}`] = value;
} else if (value != null) {
attributes[`openclaw.${key}`] = safeStringify(value);

View File

@@ -1,8 +1,6 @@
{
"id": "discord",
"channels": [
"discord"
],
"channels": ["discord"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/discord",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Discord channel plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -33,8 +33,7 @@ const meta = getChatChannelMeta("discord");
const discordMessageActions: ChannelMessageActionAdapter = {
listActions: (ctx) => getDiscordRuntime().channel.discord.messageActions.listActions(ctx),
extractToolSend: (ctx) =>
getDiscordRuntime().channel.discord.messageActions.extractToolSend(ctx),
extractToolSend: (ctx) => getDiscordRuntime().channel.discord.messageActions.extractToolSend(ctx),
handleAction: async (ctx) =>
await getDiscordRuntime().channel.discord.messageActions.handleAction(ctx),
};
@@ -281,8 +280,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
textChunkLimit: 2000,
pollMaxOptions: 10,
sendText: async ({ to, text, accountId, deps, replyToId }) => {
const send =
deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
const result = await send(to, text, {
verbose: false,
replyTo: replyToId ?? undefined,
@@ -291,8 +289,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
return { channel: "discord", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => {
const send =
deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
const result = await send(to, text, {
verbose: false,
mediaUrl,

View File

@@ -86,9 +86,7 @@ function buildAuthUrl(params: { challenge: string; state: string }): string {
return url.toString();
}
function parseCallbackInput(
input: string,
): { code: string; state: string } | { error: string } {
function parseCallbackInput(input: string): { code: string; state: string } | { error: string } {
const trimmed = input.trim();
if (!trimmed) return { error: "No input provided" };

View File

@@ -1,8 +1,6 @@
{
"id": "google-antigravity-auth",
"providers": [
"google-antigravity"
],
"providers": ["google-antigravity"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/google-antigravity-auth",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Google Antigravity OAuth provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -69,9 +69,7 @@ const geminiCliPlugin = {
},
},
defaultModel: DEFAULT_MODEL,
notes: [
"If requests fail, set GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID.",
],
notes: ["If requests fail, set GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID."],
};
} catch (err) {
spin.stop("Gemini CLI OAuth failed");

View File

@@ -67,8 +67,25 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
const geminiCliDir = dirname(dirname(resolvedPath));
const searchPaths = [
join(geminiCliDir, "node_modules", "@google", "gemini-cli-core", "dist", "src", "code_assist", "oauth2.js"),
join(geminiCliDir, "node_modules", "@google", "gemini-cli-core", "dist", "code_assist", "oauth2.js"),
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"src",
"code_assist",
"oauth2.js",
),
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"code_assist",
"oauth2.js",
),
];
let content: string | null = null;
@@ -299,7 +316,10 @@ async function waitForLocalCallback(params: {
});
}
async function exchangeCodeForTokens(code: string, verifier: string): Promise<GeminiCliOAuthCredentials> {
async function exchangeCodeForTokens(
code: string,
verifier: string,
): Promise<GeminiCliOAuthCredentials> {
const { clientId, clientSecret } = resolveOAuthClientConfig();
const body = new URLSearchParams({
client_id: clientId,
@@ -478,7 +498,9 @@ function isVpcScAffected(payload: unknown): boolean {
if (!Array.isArray(details)) return false;
return details.some(
(item) =>
typeof item === "object" && item && (item as { reason?: string }).reason === "SECURITY_POLICY_VIOLATED",
typeof item === "object" &&
item &&
(item as { reason?: string }).reason === "SECURITY_POLICY_VIOLATED",
);
}
@@ -508,7 +530,9 @@ async function pollOperation(
throw new Error("Operation polling timeout");
}
export async function loginGeminiCliOAuth(ctx: GeminiCliOAuthContext): Promise<GeminiCliOAuthCredentials> {
export async function loginGeminiCliOAuth(
ctx: GeminiCliOAuthContext,
): Promise<GeminiCliOAuthCredentials> {
const needsManual = shouldUseManualOAuthFlow(ctx.isRemote);
await ctx.note(
needsManual

View File

@@ -1,8 +1,6 @@
{
"id": "google-gemini-cli-auth",
"providers": [
"google-gemini-cli"
],
"providers": ["google-gemini-cli"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/google-gemini-cli-auth",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Gemini CLI OAuth provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -1,8 +1,6 @@
{
"id": "googlechat",
"channels": [
"googlechat"
],
"channels": ["googlechat"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,17 @@
{
"name": "@openclaw/googlechat",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Google Chat channel plugin",
"type": "module",
"dependencies": {
"google-auth-library": "^10.5.0"
},
"devDependencies": {
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.1.26"
},
"openclaw": {
"extensions": [
"./index.ts"
@@ -26,14 +35,5 @@
"localPath": "extensions/googlechat",
"defaultChoice": "npm"
}
},
"dependencies": {
"google-auth-library": "^10.5.0"
},
"devDependencies": {
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.1.26"
}
}

View File

@@ -33,7 +33,8 @@ function listEnabledAccounts(cfg: OpenClawConfig) {
function isReactionsEnabled(accounts: ReturnType<typeof listEnabledAccounts>, cfg: OpenClawConfig) {
for (const account of accounts) {
const gate = createActionGate(
(account.config.actions ?? (cfg.channels?.["googlechat"] as { actions?: unknown })?.actions) as Record<
(account.config.actions ??
(cfg.channels?.["googlechat"] as { actions?: unknown })?.actions) as Record<
string,
boolean | undefined
>,
@@ -103,7 +104,12 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
text: content,
thread: threadId ?? undefined,
attachments: upload.attachmentUploadToken
? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename }]
? [
{
attachmentUploadToken: upload.attachmentUploadToken,
contentName: loaded.filename,
},
]
: undefined,
});
return jsonResult({ ok: true, to: space });

View File

@@ -90,7 +90,8 @@ export async function verifyGoogleChatRequest(params: {
});
const payload = ticket.getPayload();
const email = payload?.email ?? "";
const ok = payload?.email_verified && (email === CHAT_ISSUER || ADDON_ISSUER_PATTERN.test(email));
const ok =
payload?.email_verified && (email === CHAT_ISSUER || ADDON_ISSUER_PATTERN.test(email));
return ok ? { ok: true } : { ok: false, reason: `invalid issuer: ${email}` };
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : "invalid token" };

View File

@@ -59,7 +59,8 @@ export const googlechatDock: ChannelDock = {
outbound: { textChunkLimit: 4000 },
config: {
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveGoogleChatAccount({ cfg: cfg as OpenClawConfig, accountId }).config.dm?.allowFrom ??
(
resolveGoogleChatAccount({ cfg: cfg as OpenClawConfig, accountId }).config.dm?.allowFrom ??
[]
).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
@@ -166,10 +167,11 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
credentialSource: account.credentialSource,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveGoogleChatAccount({
cfg: cfg as OpenClawConfig,
accountId,
}).config.dm?.allowFrom ?? []
(
resolveGoogleChatAccount({
cfg: cfg as OpenClawConfig,
accountId,
}).config.dm?.allowFrom ?? []
).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
@@ -342,7 +344,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
...next,
channels: {
...next.channels,
"googlechat": {
googlechat: {
...(next.channels?.["googlechat"] ?? {}),
enabled: true,
...configPatch,
@@ -354,7 +356,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
...next,
channels: {
...next.channels,
"googlechat": {
googlechat: {
...(next.channels?.["googlechat"] ?? {}),
enabled: true,
accounts: {
@@ -372,8 +374,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
},
outbound: {
deliveryMode: "direct",
chunker: (text, limit) =>
getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
resolveTarget: ({ to, allowFrom, mode }) => {
@@ -445,8 +446,11 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
const maxBytes = resolveChannelMediaMaxBytes({
cfg: cfg as OpenClawConfig,
resolveChannelLimitMb: ({ cfg, accountId }) =>
(cfg.channels?.["googlechat"] as { accounts?: Record<string, { mediaMaxMb?: number }>; mediaMaxMb?: number } | undefined)
?.accounts?.[accountId]?.mediaMaxMb ??
(
cfg.channels?.["googlechat"] as
| { accounts?: Record<string, { mediaMaxMb?: number }>; mediaMaxMb?: number }
| undefined
)?.accounts?.[accountId]?.mediaMaxMb ??
(cfg.channels?.["googlechat"] as { mediaMaxMb?: number } | undefined)?.mediaMaxMb,
accountId,
});

View File

@@ -4,15 +4,11 @@ import { isSenderAllowed } from "./monitor.js";
describe("isSenderAllowed", () => {
it("matches allowlist entries with users/<email>", () => {
expect(
isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"]),
).toBe(true);
expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(true);
});
it("matches allowlist entries with raw email", () => {
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(
true,
);
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(true);
});
it("still matches user id entries", () => {

View File

@@ -3,9 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
import {
type ResolvedGoogleChatAccount
} from "./accounts.js";
import { type ResolvedGoogleChatAccount } from "./accounts.js";
import {
downloadGoogleChatMedia,
deleteGoogleChatMessage,
@@ -143,7 +141,11 @@ function normalizeAudienceType(value?: string | null): GoogleChatAudienceType |
if (normalized === "app-url" || normalized === "app_url" || normalized === "app") {
return "app-url";
}
if (normalized === "project-number" || normalized === "project_number" || normalized === "project") {
if (
normalized === "project-number" ||
normalized === "project_number" ||
normalized === "project"
) {
return "project-number";
}
return undefined;
@@ -205,7 +207,7 @@ export async function handleGoogleChatWebhookRequest(
user: chat.user,
eventTime: chat.eventTime,
};
// For Add-ons, the bearer token may be in authorizationEventObject.systemIdToken
const systemIdToken = rawObj.authorizationEventObject?.systemIdToken;
if (!bearer && systemIdToken) {
@@ -322,7 +324,16 @@ export function isSenderAllowed(
function resolveGroupConfig(params: {
groupId: string;
groupName?: string | null;
groups?: Record<string, { requireMention?: boolean; allow?: boolean; enabled?: boolean; users?: Array<string | number>; systemPrompt?: string }>;
groups?: Record<
string,
{
requireMention?: boolean;
allow?: boolean;
enabled?: boolean;
users?: Array<string | number>;
systemPrompt?: string;
}
>;
}) {
const { groupId, groupName, groups } = params;
const entries = groups ?? {};
@@ -429,8 +440,7 @@ async function processMessageWithPipeline(params: {
return;
}
const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
const groupAllowed =
Boolean(groupEntry) || Boolean((account.config.groups ?? {})["*"]);
const groupAllowed = Boolean(groupEntry) || Boolean((account.config.groups ?? {})["*"]);
if (groupPolicy === "allowlist") {
if (!groupAllowlistConfigured) {
logVerbose(
@@ -451,7 +461,11 @@ async function processMessageWithPipeline(params: {
}
if (groupUsers.length > 0) {
const ok = isSenderAllowed(senderId, senderEmail, groupUsers.map((v) => String(v)));
const ok = isSenderAllowed(
senderId,
senderEmail,
groupUsers.map((v) => String(v)),
);
if (!ok) {
logVerbose(core, runtime, `drop group message (sender not allowed, ${senderId})`);
return;
@@ -625,7 +639,7 @@ async function processMessageWithPipeline(params: {
MediaPath: mediaPath,
MediaType: mediaType,
MediaUrl: mediaPath,
GroupSpace: isGroup ? space.displayName ?? undefined : undefined,
GroupSpace: isGroup ? (space.displayName ?? undefined) : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
OriginatingChannel: "googlechat",
OriginatingTo: `googlechat:${spaceId}`,
@@ -730,7 +744,8 @@ async function deliverGoogleChatReply(params: {
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
typingMessageName?: string;
}): Promise<void> {
const { payload, account, spaceId, runtime, core, config, statusSink, typingMessageName } = params;
const { payload, account, spaceId, runtime, core, config, statusSink, typingMessageName } =
params;
const mediaList = payload.mediaUrls?.length
? payload.mediaUrls
: payload.mediaUrl
@@ -801,16 +816,8 @@ async function deliverGoogleChatReply(params: {
if (payload.text) {
const chunkLimit = account.config.textChunkLimit ?? 4000;
const chunkMode = core.channel.text.resolveChunkMode(
config,
"googlechat",
account.accountId,
);
const chunks = core.channel.text.chunkMarkdownTextWithMode(
payload.text,
chunkLimit,
chunkMode,
);
const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
const chunks = core.channel.text.chunkMarkdownTextWithMode(payload.text, chunkLimit, chunkMode);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
try {
@@ -882,17 +889,19 @@ export function monitorGoogleChatProvider(options: GoogleChatMonitorOptions): ()
return unregister;
}
export async function startGoogleChatMonitor(params: GoogleChatMonitorOptions): Promise<() => void> {
export async function startGoogleChatMonitor(
params: GoogleChatMonitorOptions,
): Promise<() => void> {
return monitorGoogleChatProvider(params);
}
export function resolveGoogleChatWebhookPath(params: {
account: ResolvedGoogleChatAccount;
}): string {
return resolveWebhookPath(
params.account.config.webhookPath,
params.account.config.webhookUrl,
) ?? "/googlechat";
return (
resolveWebhookPath(params.account.config.webhookPath, params.account.config.webhookUrl) ??
"/googlechat"
);
}
export function computeGoogleChatMediaMaxMb(params: { account: ResolvedGoogleChatAccount }) {

View File

@@ -31,7 +31,7 @@ function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) {
...cfg,
channels: {
...cfg.channels,
"googlechat": {
googlechat: {
...(cfg.channels?.["googlechat"] ?? {}),
dm: {
...(cfg.channels?.["googlechat"]?.dm ?? {}),
@@ -67,7 +67,7 @@ async function promptAllowFrom(params: {
...params.cfg,
channels: {
...params.cfg.channels,
"googlechat": {
googlechat: {
...(params.cfg.channels?.["googlechat"] ?? {}),
enabled: true,
dm: {
@@ -101,7 +101,7 @@ function applyAccountConfig(params: {
...cfg,
channels: {
...cfg.channels,
"googlechat": {
googlechat: {
...(cfg.channels?.["googlechat"] ?? {}),
enabled: true,
...patch,
@@ -113,7 +113,7 @@ function applyAccountConfig(params: {
...cfg,
channels: {
...cfg.channels,
"googlechat": {
googlechat: {
...(cfg.channels?.["googlechat"] ?? {}),
enabled: true,
accounts: {
@@ -137,8 +137,7 @@ async function promptCredentials(params: {
const { cfg, prompter, accountId } = params;
const envReady =
accountId === DEFAULT_ACCOUNT_ID &&
(Boolean(process.env[ENV_SERVICE_ACCOUNT]) ||
Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE]));
(Boolean(process.env[ENV_SERVICE_ACCOUNT]) || Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE]));
if (envReady) {
const useEnv = await prompter.confirm({
message: "Use GOOGLE_CHAT_SERVICE_ACCOUNT env vars?",
@@ -173,7 +172,7 @@ async function promptCredentials(params: {
const json = await prompter.text({
message: "Service account JSON (single line)",
placeholder: "{\"type\":\"service_account\", ... }",
placeholder: '{"type":"service_account", ... }',
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
return applyAccountConfig({
@@ -237,18 +236,11 @@ export const googlechatOnboardingAdapter: ChannelOnboardingAdapter = {
return {
channel,
configured,
statusLines: [
`Google Chat: ${configured ? "configured" : "needs service account"}`,
],
statusLines: [`Google Chat: ${configured ? "configured" : "needs service account"}`],
selectionHint: configured ? "configured" : "needs auth",
};
},
configure: async ({
cfg,
prompter,
accountOverrides,
shouldPromptAccountIds,
}) => {
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
const override = accountOverrides["googlechat"]?.trim();
const defaultAccountId = resolveDefaultGoogleChatAccountId(cfg);
let accountId = override ? normalizeAccountId(override) : defaultAccountId;

View File

@@ -10,9 +10,7 @@ describe("normalizeGoogleChatTarget", () => {
it("normalizes provider prefixes", () => {
expect(normalizeGoogleChatTarget("googlechat:users/123")).toBe("users/123");
expect(normalizeGoogleChatTarget("google-chat:spaces/AAA")).toBe("spaces/AAA");
expect(normalizeGoogleChatTarget("gchat:user:User@Example.com")).toBe(
"users/user@example.com",
);
expect(normalizeGoogleChatTarget("gchat:user:User@Example.com")).toBe("users/user@example.com");
});
it("normalizes email targets to users/<email>", () => {

View File

@@ -1,8 +1,6 @@
{
"id": "imessage",
"channels": [
"imessage"
],
"channels": ["imessage"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/imessage",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw iMessage channel plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -38,10 +38,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
pairing: {
idLabel: "imessageSenderId",
notifyApproval: async ({ id }) => {
await getIMessageRuntime().channel.imessage.sendMessageIMessage(
id,
PAIRING_APPROVED_MESSAGE,
);
await getIMessageRuntime().channel.imessage.sendMessageIMessage(id, PAIRING_APPROVED_MESSAGE);
},
},
capabilities: {

View File

@@ -1,8 +1,6 @@
{
"id": "line",
"channels": [
"line"
],
"channels": ["line"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/line",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw LINE channel plugin",
"type": "module",
"devDependencies": {
"openclaw": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
@@ -22,8 +25,5 @@
"localPath": "extensions/line",
"defaultChoice": "npm"
}
},
"devDependencies": {
"openclaw": "workspace:*"
}
}

View File

@@ -221,8 +221,7 @@ export function registerLineCardCommand(api: OpenClawPluginApi): void {
const items = parseListItems(itemsStr || flags.items || "");
if (items.length === 0) {
return {
text:
'Error: List card requires items. Usage: /card list "Title" "Item1|Desc1,Item2|Desc2"',
text: 'Error: List card requires items. Usage: /card list "Title" "Item1|Desc1,Item2|Desc2"',
};
}
const bubble = createListCard(title, items);
@@ -242,8 +241,7 @@ export function registerLineCardCommand(api: OpenClawPluginApi): void {
if (items.length === 0) {
return {
text:
'Error: Receipt card requires items. Usage: /card receipt "Title" "Item1:$10,Item2:$20" --total "$30"',
text: 'Error: Receipt card requires items. Usage: /card receipt "Title" "Item1:$10,Item2:$20" --total "$30"',
};
}

View File

@@ -12,24 +12,25 @@ type LineRuntimeMocks = {
function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
const writeConfigFile = vi.fn(async () => {});
const resolveLineAccount = vi.fn(({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const lineConfig = (cfg.channels?.line ?? {}) as {
tokenFile?: string;
secretFile?: string;
channelAccessToken?: string;
channelSecret?: string;
accounts?: Record<string, Record<string, unknown>>;
};
const entry =
accountId && accountId !== DEFAULT_ACCOUNT_ID
? lineConfig.accounts?.[accountId] ?? {}
: lineConfig;
const hasToken =
Boolean((entry as any).channelAccessToken) || Boolean((entry as any).tokenFile);
const hasSecret =
Boolean((entry as any).channelSecret) || Boolean((entry as any).secretFile);
return { tokenSource: hasToken && hasSecret ? "config" : "none" };
});
const resolveLineAccount = vi.fn(
({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const lineConfig = (cfg.channels?.line ?? {}) as {
tokenFile?: string;
secretFile?: string;
channelAccessToken?: string;
channelSecret?: string;
accounts?: Record<string, Record<string, unknown>>;
};
const entry =
accountId && accountId !== DEFAULT_ACCOUNT_ID
? (lineConfig.accounts?.[accountId] ?? {})
: lineConfig;
const hasToken =
Boolean((entry as any).channelAccessToken) || Boolean((entry as any).tokenFile);
const hasSecret = Boolean((entry as any).channelSecret) || Boolean((entry as any).secretFile);
return { tokenSource: hasToken && hasSecret ? "config" : "none" };
},
);
const runtime = {
config: { writeConfigFile },

View File

@@ -33,18 +33,19 @@ function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
const sendMessageLine = vi.fn(async () => ({ messageId: "m-media", chatId: "c1" }));
const chunkMarkdownText = vi.fn((text: string) => [text]);
const resolveTextChunkLimit = vi.fn(() => 123);
const resolveLineAccount = vi.fn(({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const resolved = accountId ?? "default";
const lineConfig = (cfg.channels?.line ?? {}) as {
accounts?: Record<string, Record<string, unknown>>;
};
const accountConfig =
resolved !== "default" ? lineConfig.accounts?.[resolved] ?? {} : {};
return {
accountId: resolved,
config: { ...lineConfig, ...accountConfig },
};
});
const resolveLineAccount = vi.fn(
({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const resolved = accountId ?? "default";
const lineConfig = (cfg.channels?.line ?? {}) as {
accounts?: Record<string, Record<string, unknown>>;
};
const accountConfig = resolved !== "default" ? (lineConfig.accounts?.[resolved] ?? {}) : {};
return {
accountId: resolved,
config: { ...lineConfig, ...accountConfig },
};
},
);
const runtime = {
channel: {
@@ -256,12 +257,9 @@ describe("linePlugin outbound.sendPayload", () => {
cfg,
});
expect(mocks.resolveTextChunkLimit).toHaveBeenCalledWith(
cfg,
"line",
"primary",
{ fallbackLimit: 5000 },
);
expect(mocks.resolveTextChunkLimit).toHaveBeenCalledWith(cfg, "line", "primary", {
fallbackLimit: 5000,
});
expect(mocks.chunkMarkdownText).toHaveBeenCalledWith("Hello world", 123);
});
});

View File

@@ -139,9 +139,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
tokenSource: account.tokenSource,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }).config.allowFrom ?? []).map(
(entry) => String(entry),
),
(
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }).config.allowFrom ?? []
).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
@@ -170,8 +170,8 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
};
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy =
(cfg.channels?.defaults as { groupPolicy?: string } | undefined)?.groupPolicy;
const defaultGroupPolicy = (cfg.channels?.defaults as { groupPolicy?: string } | undefined)
?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
return [
@@ -371,14 +371,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
: { text: "", flexMessages: [] };
const chunkLimit =
runtime.channel.text.resolveTextChunkLimit?.(
cfg,
"line",
accountId ?? undefined,
{
fallbackLimit: 5000,
},
) ?? 5000;
runtime.channel.text.resolveTextChunkLimit?.(cfg, "line", accountId ?? undefined, {
fallbackLimit: 5000,
}) ?? 5000;
const chunks = processed.text
? runtime.channel.text.chunkMarkdownText(processed.text, chunkLimit)

View File

@@ -8,7 +8,7 @@ Designed to be called from workflow engines (for example, Lobster via
## Enable
1) Enable the plugin:
1. Enable the plugin:
```json
{
@@ -20,7 +20,7 @@ Designed to be called from workflow engines (for example, Lobster via
}
```
2) Allowlist the tool (it is registered with `optional: true`):
2. Allowlist the tool (it is registered with `optional: true`):
```json
{

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/llm-task",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -17,7 +17,9 @@ function fakeApi(overrides: any = {}) {
id: "llm-task",
name: "llm-task",
source: "test",
config: { agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } } },
config: {
agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } },
},
pluginConfig: {},
runtime: { version: "test" },
logger: { debug() {}, info() {}, warn() {}, error() {} },
@@ -42,7 +44,7 @@ describe("llm-task tool (json-only)", () => {
it("strips fenced json", async () => {
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
meta: {},
payloads: [{ text: "```json\n{\"ok\":true}\n```" }],
payloads: [{ text: '```json\n{"ok":true}\n```' }],
});
const tool = createLlmTaskTool(fakeApi() as any);
const res = await tool.execute("id", { prompt: "return ok" });
@@ -66,7 +68,10 @@ describe("llm-task tool (json-only)", () => {
});
it("throws on invalid json", async () => {
(runEmbeddedPiAgent as any).mockResolvedValueOnce({ meta: {}, payloads: [{ text: "not-json" }] });
(runEmbeddedPiAgent as any).mockResolvedValueOnce({
meta: {},
payloads: [{ text: "not-json" }],
});
const tool = createLlmTaskTool(fakeApi() as any);
await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i);
});
@@ -98,10 +103,12 @@ describe("llm-task tool (json-only)", () => {
meta: {},
payloads: [{ text: JSON.stringify({ ok: true }) }],
});
const tool = createLlmTaskTool(fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any);
await expect(tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" })).rejects.toThrow(
/not allowed/i,
const tool = createLlmTaskTool(
fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any,
);
await expect(
tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" }),
).rejects.toThrow(/not allowed/i);
});
it("disables tools for embedded run", async () => {

View File

@@ -18,7 +18,8 @@ async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
// Source checkout (tests/dev)
try {
const mod = await import("../../../src/agents/pi-embedded-runner.js");
if (typeof (mod as any).runEmbeddedPiAgent === "function") return (mod as any).runEmbeddedPiAgent;
if (typeof (mod as any).runEmbeddedPiAgent === "function")
return (mod as any).runEmbeddedPiAgent;
} catch {
// ignore
}
@@ -69,8 +70,12 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
parameters: Type.Object({
prompt: Type.String({ description: "Task instruction for the LLM." }),
input: Type.Optional(Type.Unknown({ description: "Optional input payload for the task." })),
schema: Type.Optional(Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." })),
provider: Type.Optional(Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." })),
schema: Type.Optional(
Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." }),
),
provider: Type.Optional(
Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." }),
),
model: Type.Optional(Type.String({ description: "Model id override." })),
authProfileId: Type.Optional(Type.String({ description: "Auth profile override." })),
temperature: Type.Optional(Type.Number({ description: "Best-effort temperature override." })),
@@ -86,7 +91,8 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
const primary = api.config?.agents?.defaults?.model?.primary;
const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined;
const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
const primaryModel =
typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined;
const provider =
(typeof params.provider === "string" && params.provider.trim()) ||
@@ -101,8 +107,10 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
undefined;
const authProfileId =
(typeof (params as any).authProfileId === "string" && (params as any).authProfileId.trim()) ||
(typeof pluginCfg.defaultAuthProfileId === "string" && pluginCfg.defaultAuthProfileId.trim()) ||
(typeof (params as any).authProfileId === "string" &&
(params as any).authProfileId.trim()) ||
(typeof pluginCfg.defaultAuthProfileId === "string" &&
pluginCfg.defaultAuthProfileId.trim()) ||
undefined;
const modelKey = toModelKey(provider, model);
@@ -120,8 +128,12 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
}
const timeoutMs =
(typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : undefined) ||
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0 ? pluginCfg.timeoutMs : undefined) ||
(typeof params.timeoutMs === "number" && params.timeoutMs > 0
? params.timeoutMs
: undefined) ||
(typeof pluginCfg.timeoutMs === "number" && pluginCfg.timeoutMs > 0
? pluginCfg.timeoutMs
: undefined) ||
30_000;
const streamParams = {
@@ -194,8 +206,9 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
const ok = validate(parsed);
if (!ok) {
const msg =
validate.errors?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`).join("; ") ??
"invalid";
validate.errors
?.map((e) => `${e.instancePath || "<root>"} ${e.message || "invalid"}`)
.join("; ") ?? "invalid";
throw new Error(`LLM JSON did not match schema: ${msg}`);
}
}

View File

@@ -5,7 +5,7 @@ Adds the `lobster` agent tool as an **optional** plugin tool.
## What this is
- Lobster is a standalone workflow shell (typed JSON-first pipelines + approvals/resume).
- This plugin integrates Lobster with OpenClaw *without core changes*.
- This plugin integrates Lobster with OpenClaw _without core changes_.
## Enable
@@ -53,22 +53,17 @@ Example (allow only a small set of tools):
{
"id": "main",
"tools": {
"allow": [
"lobster",
"web_fetch",
"web_search",
"gog",
"gh"
],
"deny": ["gateway"]
}
}
]
}
"allow": ["lobster", "web_fetch", "web_search", "gog", "gh"],
"deny": ["gateway"],
},
},
],
},
}
```
Notes:
- If `tools.allow` is omitted or empty, it behaves like "allow everything (except denied)". For a real allowlist, set a **non-empty** `allow`.
- Tool names depend on which plugins you have installed/enabled.

View File

@@ -8,13 +8,13 @@ Lobster executes multi-step workflows with approval checkpoints. Use it when:
## When to use Lobster
| User intent | Use Lobster? |
|-------------|--------------|
| "Triage my email" | Yes — multi-step, may send replies |
| "Send a message" | No — single action, use message tool directly |
| "Check my email every morning and ask before replying" | Yes — scheduled workflow with approval |
| "What's the weather?" | No — simple query |
| "Monitor this PR and notify me of changes" | Yes — stateful, recurring |
| User intent | Use Lobster? |
| ------------------------------------------------------ | --------------------------------------------- |
| "Triage my email" | Yes — multi-step, may send replies |
| "Send a message" | No — single action, use message tool directly |
| "Check my email every morning and ask before replying" | Yes — scheduled workflow with approval |
| "What's the weather?" | No — simple query |
| "Monitor this PR and notify me of changes" | Yes — stateful, recurring |
## Basic usage
@@ -28,6 +28,7 @@ Lobster executes multi-step workflows with approval checkpoints. Use it when:
```
Returns structured result:
```json
{
"protocolVersion": 1,
@@ -41,6 +42,7 @@ Returns structured result:
### Handle approval
If the workflow needs approval:
```json
{
"status": "needs_approval",
@@ -54,6 +56,7 @@ If the workflow needs approval:
```
Present the prompt to the user. If they approve:
```json
{
"action": "resume",
@@ -65,15 +68,19 @@ Present the prompt to the user. If they approve:
## Example workflows
### Email triage
```
gog.gmail.search --query 'newer_than:1d' --max 20 | email.triage
```
Fetches recent emails, classifies into buckets (needs_reply, needs_action, fyi).
### Email triage with approval gate
```
gog.gmail.search --query 'newer_than:1d' | email.triage | approve --prompt 'Process these?'
```
Same as above, but halts for approval before returning.
## Key behaviors

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/lobster",
"version": "2026.1.30",
"type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -192,9 +192,11 @@ export function createLobsterTool(api: OpenClawPluginApi) {
const execPath = resolveExecutablePath(
typeof params.lobsterPath === "string" ? params.lobsterPath : undefined,
);
const cwd = typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : process.cwd();
const cwd =
typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : process.cwd();
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
const maxStdoutBytes = typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
const maxStdoutBytes =
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
const argv = (() => {
if (action === "run") {

View File

@@ -3,56 +3,67 @@
## 2026.1.30
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.29
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.23
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.22
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.21
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.20
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.17-1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.17
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.15
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.14
### Features
- Version alignment with core OpenClaw release numbers.
- Matrix channel plugin with homeserver + user ID auth (access token or password login with device name).
- Direct messages with pairing/allowlist/open/disabled policies and allowFrom support.

View File

@@ -1,8 +1,6 @@
{
"id": "matrix",
"channels": [
"matrix"
],
"channels": ["matrix"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,18 @@
{
"name": "@openclaw/matrix",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Matrix channel plugin",
"type": "module",
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
"markdown-it": "14.1.0",
"music-metadata": "^11.11.1",
"zod": "^4.3.6"
},
"devDependencies": {
"openclaw": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
@@ -22,15 +32,5 @@
"localPath": "extensions/matrix",
"defaultChoice": "npm"
}
},
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
"markdown-it": "14.1.0",
"music-metadata": "^11.11.1",
"zod": "^4.3.6"
},
"devDependencies": {
"openclaw": "workspace:*"
}
}

View File

@@ -34,7 +34,12 @@ describe("matrix directory", () => {
expect(matrixPlugin.directory?.listGroups).toBeTruthy();
await expect(
matrixPlugin.directory!.listPeers({ cfg, accountId: undefined, query: undefined, limit: undefined }),
matrixPlugin.directory!.listPeers({
cfg,
accountId: undefined,
query: undefined,
limit: undefined,
}),
).resolves.toEqual(
expect.arrayContaining([
{ kind: "user", id: "user:@alice:example.org" },
@@ -45,7 +50,12 @@ describe("matrix directory", () => {
);
await expect(
matrixPlugin.directory!.listGroups({ cfg, accountId: undefined, query: undefined, limit: undefined }),
matrixPlugin.directory!.listGroups({
cfg,
accountId: undefined,
query: undefined,
limit: undefined,
}),
).resolves.toEqual(
expect.arrayContaining([
{ kind: "group", id: "room:!room1:example.org" },

View File

@@ -12,7 +12,10 @@ import {
import { matrixMessageActions } from "./actions.js";
import { MatrixConfigSchema } from "./config-schema.js";
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
import {
resolveMatrixGroupRequireMention,
resolveMatrixGroupToolPolicy,
} from "./group-mentions.js";
import type { CoreConfig } from "./types.js";
import {
listMatrixAccountIds,
@@ -27,10 +30,7 @@ import { sendMessageMatrix } from "./matrix/send.js";
import { matrixOnboardingAdapter } from "./onboarding.js";
import { matrixOutbound } from "./outbound.js";
import { resolveMatrixTargets } from "./resolve-targets.js";
import {
listMatrixDirectoryGroupsLive,
listMatrixDirectoryPeersLive,
} from "./directory-live.js";
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
const meta = {
id: "matrix",
@@ -108,8 +108,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
configSchema: buildChannelConfigSchema(MatrixConfigSchema),
config: {
listAccountIds: (cfg) => listMatrixAccountIds(cfg as CoreConfig),
resolveAccount: (cfg, accountId) =>
resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId }),
resolveAccount: (cfg, accountId) => resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId }),
defaultAccountId: (cfg) => resolveDefaultMatrixAccountId(cfg as CoreConfig),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
@@ -153,15 +152,18 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
policyPath: "channels.matrix.dm.policy",
allowFromPath: "channels.matrix.dm.allowFrom",
approveHint: formatPairingApproveHint("matrix"),
normalizeEntry: (raw) => raw.replace(/^matrix:/i, "").trim().toLowerCase(),
normalizeEntry: (raw) =>
raw
.replace(/^matrix:/i, "")
.trim()
.toLowerCase(),
}),
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
const groupPolicy =
account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
return [
"- Matrix rooms: groupPolicy=\"open\" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy=\"allowlist\" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.",
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.',
];
},
},
@@ -170,16 +172,13 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
resolveToolPolicy: resolveMatrixGroupToolPolicy,
},
threading: {
resolveReplyToMode: ({ cfg }) =>
(cfg as CoreConfig).channels?.matrix?.replyToMode ?? "off",
resolveReplyToMode: ({ cfg }) => (cfg as CoreConfig).channels?.matrix?.replyToMode ?? "off",
buildToolContext: ({ context, hasRepliedRef }) => {
const currentTarget = context.To;
return {
currentChannelId: currentTarget?.trim() || undefined,
currentThreadTs:
context.MessageThreadId != null
? String(context.MessageThreadId)
: context.ReplyToId,
context.MessageThreadId != null ? String(context.MessageThreadId) : context.ReplyToId,
hasRepliedRef,
};
},
@@ -399,9 +398,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
accountId: account.accountId,
baseUrl: account.homeserver,
});
ctx.log?.info(
`[${account.accountId}] starting provider (${account.homeserver ?? "matrix"})`,
);
ctx.log?.info(`[${account.accountId}] starting provider (${account.homeserver ?? "matrix"})`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorMatrixProvider } = await import("./matrix/index.js");
return monitorMatrixProvider({

View File

@@ -96,7 +96,7 @@ export async function readMatrixMessages(
const token = opts.before?.trim() || opts.after?.trim() || undefined;
const dir = opts.after ? "f" : "b";
// @vector-im/matrix-bot-sdk uses doRequest for room messages
const res = await client.doRequest(
const res = (await client.doRequest(
"GET",
`/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`,
{
@@ -104,7 +104,7 @@ export async function readMatrixMessages(
limit,
from: token,
},
) as { chunk: MatrixRawEvent[]; start?: string; end?: string };
)) as { chunk: MatrixRawEvent[]; start?: string; end?: string };
const messages = res.chunk
.filter((event) => event.type === EventType.RoomMessage)
.filter((event) => !event.unsigned?.redacted_because)

View File

@@ -22,11 +22,11 @@ export async function listMatrixReactions(
? Math.max(1, Math.floor(opts.limit))
: 100;
// @vector-im/matrix-bot-sdk uses doRequest for relations
const res = await client.doRequest(
const res = (await client.doRequest(
"GET",
`/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`,
{ dir: "b", limit },
) as { chunk: MatrixRawEvent[] };
)) as { chunk: MatrixRawEvent[] };
const summaries = new Map<string, MatrixReactionSummary>();
for (const event of res.chunk) {
const content = event.content as ReactionEventContent;
@@ -58,11 +58,11 @@ export async function removeMatrixReactions(
const { client, stopOnDone } = await resolveActionClient(opts);
try {
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
const res = await client.doRequest(
const res = (await client.doRequest(
"GET",
`/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`,
{ dir: "b", limit: 200 },
) as { chunk: MatrixRawEvent[] };
)) as { chunk: MatrixRawEvent[] };
const userId = await client.getUserId();
if (!userId) return { removed: 0 };
const targetEmoji = opts.emoji?.trim();

View File

@@ -29,10 +29,7 @@ export async function getMatrixMemberInfo(
}
}
export async function getMatrixRoomInfo(
roomId: string,
opts: MatrixActionClientOpts = {},
) {
export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClientOpts = {}) {
const { client, stopOnDone } = await resolveActionClient(opts);
try {
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
@@ -57,11 +54,7 @@ export async function getMatrixRoomInfo(
}
try {
const aliasState = await client.getRoomStateEvent(
resolvedRoom,
"m.room.canonical_alias",
"",
);
const aliasState = await client.getRoomStateEvent(resolvedRoom, "m.room.canonical_alias", "");
canonicalAlias = aliasState?.alias ?? null;
} catch {
// ignore

View File

@@ -38,10 +38,7 @@ export function summarizeMatrixRawEvent(event: MatrixRawEvent): MatrixMessageSum
};
}
export async function readPinnedEvents(
client: MatrixClient,
roomId: string,
): Promise<string[]> {
export async function readPinnedEvents(client: MatrixClient, roomId: string): Promise<string[]> {
try {
const content = (await client.getRoomStateEvent(
roomId,

View File

@@ -2,8 +2,4 @@ export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
export { isBunRuntime } from "./client/runtime.js";
export { resolveMatrixConfig, resolveMatrixAuth } from "./client/config.js";
export { createMatrixClient } from "./client/create-client.js";
export {
resolveSharedMatrixClient,
waitForMatrixSync,
stopSharedClient,
} from "./client/shared.js";
export { resolveSharedMatrixClient, waitForMatrixSync, stopSharedClient } from "./client/shared.js";

View File

@@ -16,11 +16,9 @@ export function resolveMatrixConfig(
const matrix = cfg.channels?.matrix ?? {};
const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
const accessToken =
clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
const password = clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined;
const deviceName =
clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined;
const deviceName = clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined;
const initialSyncLimit =
typeof matrix.initialSyncLimit === "number"
? Math.max(0, Math.floor(matrix.initialSyncLimit))
@@ -106,9 +104,7 @@ export async function resolveMatrixAuth(params?: {
}
if (!resolved.userId) {
throw new Error(
"Matrix userId is required when no access token is configured (matrix.userId)",
);
throw new Error("Matrix userId is required when no access token is configured (matrix.userId)");
}
if (!resolved.password) {

View File

@@ -66,12 +66,13 @@ export async function createMatrixClient(params: {
try {
const { StoreType } = await import("@matrix-org/matrix-sdk-crypto-nodejs");
cryptoStorage = new RustSdkCryptoStorageProvider(
storagePaths.cryptoPath,
StoreType.Sqlite,
);
cryptoStorage = new RustSdkCryptoStorageProvider(storagePaths.cryptoPath, StoreType.Sqlite);
} catch (err) {
LogService.warn("MatrixClientLite", "Failed to initialize crypto storage, E2EE disabled:", err);
LogService.warn(
"MatrixClientLite",
"Failed to initialize crypto storage, E2EE disabled:",
err,
);
}
}
@@ -82,12 +83,7 @@ export async function createMatrixClient(params: {
accountId: params.accountId,
});
const client = new MatrixClient(
params.homeserver,
params.accessToken,
storage,
cryptoStorage,
);
const client = new MatrixClient(params.homeserver, params.accessToken, storage, cryptoStorage);
if (client.crypto) {
const originalUpdateSyncData = client.crypto.updateSyncData.bind(client.crypto);

View File

@@ -3,10 +3,7 @@ import { ConsoleLogger, LogService } from "@vector-im/matrix-bot-sdk";
let matrixSdkLoggingConfigured = false;
const matrixSdkBaseLogger = new ConsoleLogger();
function shouldSuppressMatrixHttpNotFound(
module: string,
messageOrObject: unknown[],
): boolean {
function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean {
if (module !== "MatrixHttpClient") return false;
return messageOrObject.some((entry) => {
if (!entry || typeof entry !== "object") return false;
@@ -19,14 +16,10 @@ export function ensureMatrixSdkLoggingConfigured(): void {
matrixSdkLoggingConfigured = true;
LogService.setLogger({
trace: (module, ...messageOrObject) =>
matrixSdkBaseLogger.trace(module, ...messageOrObject),
debug: (module, ...messageOrObject) =>
matrixSdkBaseLogger.debug(module, ...messageOrObject),
info: (module, ...messageOrObject) =>
matrixSdkBaseLogger.info(module, ...messageOrObject),
warn: (module, ...messageOrObject) =>
matrixSdkBaseLogger.warn(module, ...messageOrObject),
trace: (module, ...messageOrObject) => matrixSdkBaseLogger.trace(module, ...messageOrObject),
debug: (module, ...messageOrObject) => matrixSdkBaseLogger.debug(module, ...messageOrObject),
info: (module, ...messageOrObject) => matrixSdkBaseLogger.info(module, ...messageOrObject),
warn: (module, ...messageOrObject) => matrixSdkBaseLogger.warn(module, ...messageOrObject),
error: (module, ...messageOrObject) => {
if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) return;
matrixSdkBaseLogger.error(module, ...messageOrObject);

View File

@@ -82,8 +82,7 @@ export function maybeMigrateLegacyStorage(params: {
const hasLegacyStorage = fs.existsSync(legacy.storagePath);
const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
const hasNewStorage =
fs.existsSync(params.storagePaths.storagePath) ||
fs.existsSync(params.storagePaths.cryptoPath);
fs.existsSync(params.storagePaths.storagePath) || fs.existsSync(params.storagePaths.cryptoPath);
if (!hasLegacyStorage && !hasLegacyCrypto) return;
if (hasNewStorage) return;
@@ -120,11 +119,7 @@ export function writeStorageMeta(params: {
createdAt: new Date().toISOString(),
};
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
fs.writeFileSync(
params.storagePaths.metaPath,
JSON.stringify(payload, null, 2),
"utf-8",
);
fs.writeFileSync(params.storagePaths.metaPath, JSON.stringify(payload, null, 2), "utf-8");
} catch {
// ignore meta write failures
}

View File

@@ -19,8 +19,7 @@ export function resolveMatrixCredentialsDir(
env: NodeJS.ProcessEnv = process.env,
stateDir?: string,
): string {
const resolvedStateDir =
stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
const resolvedStateDir = stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
return path.join(resolvedStateDir, "credentials", "matrix");
}

View File

@@ -52,6 +52,8 @@ export async function ensureMatrixSdkInstalled(params: {
);
}
if (!isMatrixSdkAvailable()) {
throw new Error("Matrix dependency install completed but @vector-im/matrix-bot-sdk is still missing.");
throw new Error(
"Matrix dependency install completed but @vector-im/matrix-bot-sdk is still missing.",
);
}
}

View File

@@ -12,10 +12,7 @@ type DirectRoomTrackerOptions = {
const DM_CACHE_TTL_MS = 30_000;
export function createDirectRoomTracker(
client: MatrixClient,
opts: DirectRoomTrackerOptions = {},
) {
export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTrackerOptions = {}) {
const log = opts.log ?? (() => {});
let lastDmUpdateMs = 0;
let cachedSelfUserId: string | null = null;
@@ -94,11 +91,7 @@ export function createDirectRoomTracker(
return true;
}
log(
`matrix: dm check room=${roomId} result=group members=${
memberCount ?? "unknown"
}`,
);
log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);
return false;
},
};

View File

@@ -84,8 +84,7 @@ export function registerMatrixMonitorEvents(params: {
const hint = formatNativeDependencyHint({
packageName: "@matrix-org/matrix-sdk-crypto-nodejs",
manager: "pnpm",
downloadCommand:
"node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js",
downloadCommand: "node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js",
});
const warning = `matrix: encryption enabled but crypto is unavailable; ${hint}`;
logger.warn({ roomId }, warning);

View File

@@ -16,7 +16,12 @@ import {
parsePollStartContent,
type PollStartContent,
} from "../poll-types.js";
import { reactMatrixMessage, sendMessageMatrix, sendReadReceiptMatrix, sendTypingMatrix } from "../send.js";
import {
reactMatrixMessage,
sendMessageMatrix,
sendReadReceiptMatrix,
sendTypingMatrix,
} from "../send.js";
import {
resolveMatrixAllowListMatch,
resolveMatrixAllowListMatches,
@@ -37,7 +42,7 @@ export type MatrixMonitorHandlerParams = {
logging: {
shouldLogVerbose: () => boolean;
};
channel: typeof import("openclaw/plugin-sdk")["channel"];
channel: (typeof import("openclaw/plugin-sdk"))["channel"];
system: {
enqueueSystemEvent: (
text: string,
@@ -59,7 +64,7 @@ export type MatrixMonitorHandlerParams = {
: Record<string, unknown> | undefined
: Record<string, unknown> | undefined;
mentionRegexes: ReturnType<
typeof import("openclaw/plugin-sdk")["channel"]["mentions"]["buildMentionRegexes"]
(typeof import("openclaw/plugin-sdk"))["channel"]["mentions"]["buildMentionRegexes"]
>;
groupPolicy: "open" | "allowlist" | "disabled";
replyToMode: ReplyToMode;
@@ -77,7 +82,9 @@ export type MatrixMonitorHandlerParams = {
selfUserId: string;
}) => Promise<boolean>;
};
getRoomInfo: (roomId: string) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
getRoomInfo: (
roomId: string,
) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
};
@@ -118,8 +125,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const locationContent = event.content as LocationMessageEventContent;
const isLocationEvent =
eventType === EventType.Location ||
(eventType === EventType.RoomMessage &&
locationContent.msgtype === EventType.Location);
(eventType === EventType.RoomMessage && locationContent.msgtype === EventType.Location);
if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) return;
logVerboseMessage(
`matrix: room.message recv room=${roomId} type=${eventType} id=${event.event_id ?? "unknown"}`,
@@ -144,10 +150,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const roomInfo = await getRoomInfo(roomId);
const roomName = roomInfo.name;
const roomAliases = [
roomInfo.canonicalAlias ?? "",
...roomInfo.altAliases,
].filter(Boolean);
const roomAliases = [roomInfo.canonicalAlias ?? "", ...roomInfo.altAliases].filter(Boolean);
let content = event.content as RoomMessageEventContent;
if (isPollEvent) {
@@ -219,7 +222,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
const senderName = await getMemberDisplayName(roomId, senderId);
const storeAllowFrom = await core.channel.pairing.readAllowFromStore("matrix").catch(() => []);
const storeAllowFrom = await core.channel.pairing
.readAllowFromStore("matrix")
.catch(() => []);
const effectiveAllowFrom = normalizeAllowListLower([...allowFrom, ...storeAllowFrom]);
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
const effectiveGroupAllowFrom = normalizeAllowListLower([
@@ -311,8 +316,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`);
}
const rawBody = locationPayload?.text
?? (typeof content.body === "string" ? content.body.trim() : "");
const rawBody =
locationPayload?.text ?? (typeof content.body === "string" ? content.body.trim() : "");
let media: {
path: string;
contentType?: string;
@@ -334,8 +339,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
? (content.info as { mimetype?: string; size?: number })
: undefined;
const contentType = contentInfo?.mimetype;
const contentSize =
typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
if (mediaUrl?.startsWith("mxc://")) {
try {
media = await downloadMatrixMedia({
@@ -514,7 +518,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
: undefined,
onRecordError: (err) => {
logger.warn(
{ error: String(err), storePath, sessionKey: ctxPayload.SessionKey ?? route.sessionKey },
{
error: String(err),
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
},
"failed updating session meta",
);
},
@@ -528,16 +536,16 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const shouldAckReaction = () =>
Boolean(
ackReaction &&
core.channel.reactions.shouldAckReaction({
scope: ackScope,
isDirect: isDirectMessage,
isGroup: isRoom,
isMentionableGroup: isRoom,
requireMention: Boolean(shouldRequireMention),
canDetectMention,
effectiveWasMentioned: wasMentioned || shouldBypassMention,
shouldBypassMention,
}),
core.channel.reactions.shouldAckReaction({
scope: ackScope,
isDirect: isDirectMessage,
isGroup: isRoom,
isMentionableGroup: isRoom,
requireMention: Boolean(shouldRequireMention),
canDetectMention,
effectiveWasMentioned: wasMentioned || shouldBypassMention,
shouldBypassMention,
}),
);
if (shouldAckReaction() && messageId) {
reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => {

View File

@@ -1,10 +1,6 @@
import { format } from "node:util";
import {
mergeAllowlist,
summarizeMapping,
type RuntimeEnv,
} from "openclaw/plugin-sdk";
import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plugin-sdk";
import type { CoreConfig, ReplyToMode } from "../../types.js";
import { setActiveMatrixClient } from "../active-client.js";
import {
@@ -59,9 +55,15 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
};
const normalizeUserEntry = (raw: string) =>
raw.replace(/^matrix:/i, "").replace(/^user:/i, "").trim();
raw
.replace(/^matrix:/i, "")
.replace(/^user:/i, "")
.trim();
const normalizeRoomEntry = (raw: string) =>
raw.replace(/^matrix:/i, "").replace(/^(room|channel):/i, "").trim();
raw
.replace(/^matrix:/i, "")
.replace(/^(room|channel):/i, "")
.trim();
const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":");
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
@@ -256,7 +258,10 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
logger.info("matrix: device verification requested - please verify in another client");
}
} catch (err) {
logger.debug({ error: String(err) }, "Device verification request failed (may already be verified)");
logger.debug(
{ error: String(err) },
"Device verification request failed (may already be verified)",
);
}
}

View File

@@ -74,10 +74,7 @@ export async function downloadMatrixMedia(params: {
placeholder: string;
} | null> {
let fetched: { buffer: Buffer; headerType?: string } | null;
if (
typeof params.sizeBytes === "number" &&
params.sizeBytes > params.maxBytes
) {
if (typeof params.sizeBytes === "number" && params.sizeBytes > params.maxBytes) {
throw new Error("Matrix media exceeds configured size limit");
}

View File

@@ -16,9 +16,7 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) {
let canonicalAlias: string | undefined;
let altAliases: string[] = [];
try {
const nameState = await client
.getRoomStateEvent(roomId, "m.room.name", "")
.catch(() => null);
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "").catch(() => null);
name = nameState?.name;
} catch {
// ignore
@@ -37,10 +35,7 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) {
return info;
};
const getMemberDisplayName = async (
roomId: string,
userId: string,
): Promise<string> => {
const getMemberDisplayName = async (roomId: string, userId: string): Promise<string> => {
try {
const memberState = await client
.getRoomStateEvent(roomId, "m.room.member", userId)

View File

@@ -24,7 +24,12 @@ export function resolveMatrixRoomConfig(params: {
...params.aliases,
params.name ?? "",
);
const { entry: matched, key: matchedKey, wildcardEntry, wildcardKey } = resolveChannelEntryMatch({
const {
entry: matched,
key: matchedKey,
wildcardEntry,
wildcardKey,
} = resolveChannelEntryMatch({
entries: rooms,
keys: candidates,
wildcardKey: "*",

View File

@@ -82,9 +82,10 @@ export function getTextContent(text?: TextContent): string {
}
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
const poll = (content as Record<string, PollStartSubtype | undefined>)[M_POLL_START]
?? (content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START]
?? (content as Record<string, PollStartSubtype | undefined>)["m.poll"];
const poll =
(content as Record<string, PollStartSubtype | undefined>)[M_POLL_START] ??
(content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START] ??
(content as Record<string, PollStartSubtype | undefined>)["m.poll"];
if (!poll) return null;
const question = getTextContent(poll.question);

View File

@@ -13,10 +13,7 @@ import {
const getCore = () => getMatrixRuntime();
export function buildTextContent(
body: string,
relation?: MatrixRelation,
): MatrixTextContent {
export function buildTextContent(body: string, relation?: MatrixRelation): MatrixTextContent {
const content: MatrixTextContent = relation
? {
msgtype: MsgType.Text,
@@ -44,23 +41,17 @@ export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | un
return { "m.in_reply_to": { event_id: trimmed } };
}
export function buildThreadRelation(
threadId: string,
replyToId?: string,
): MatrixThreadRelation {
export function buildThreadRelation(threadId: string, replyToId?: string): MatrixThreadRelation {
const trimmed = threadId.trim();
return {
rel_type: RelationType.Thread,
event_id: trimmed,
is_falling_back: true,
"m.in_reply_to": { event_id: (replyToId?.trim() || trimmed) },
"m.in_reply_to": { event_id: replyToId?.trim() || trimmed },
};
}
export function resolveMatrixMsgType(
contentType?: string,
_fileName?: string,
): MatrixMediaMsgType {
export function resolveMatrixMsgType(contentType?: string, _fileName?: string): MatrixMediaMsgType {
const kind = getCore().media.mediaKindFromMime(contentType ?? "");
switch (kind) {
case "image":

View File

@@ -113,7 +113,9 @@ export async function prepareImageInfo(params: {
buffer: Buffer;
client: MatrixClient;
}): Promise<DimensionalFileInfo | undefined> {
const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null);
const meta = await getCore()
.media.getImageMetadata(params.buffer)
.catch(() => null);
if (!meta) return undefined;
const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
const maxDim = Math.max(meta.width, meta.height);
@@ -125,7 +127,9 @@ export async function prepareImageInfo(params: {
quality: THUMBNAIL_QUALITY,
withoutEnlargement: true,
});
const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null);
const thumbMeta = await getCore()
.media.getImageMetadata(thumbBuffer)
.catch(() => null);
const thumbUri = await params.client.uploadContent(
thumbBuffer,
"image/jpeg",
@@ -201,7 +205,7 @@ export async function uploadMediaMaybeEncrypted(
},
): Promise<{ url: string; file?: EncryptedFile }> {
// Check if room is encrypted and crypto is available
const isEncrypted = client.crypto && await client.crypto.isRoomEncrypted(roomId);
const isEncrypted = client.crypto && (await client.crypto.isRoomEncrypted(roomId));
if (isEncrypted && client.crypto) {
// Encrypt the media before uploading

View File

@@ -37,10 +37,7 @@ describe("resolveMatrixRoomId", () => {
const client = {
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
getJoinedRoomMembers: vi.fn().mockResolvedValue([
"@bot:example.org",
userId,
]),
getJoinedRoomMembers: vi.fn().mockResolvedValue(["@bot:example.org", userId]),
setAccountData,
} as unknown as MatrixClient;
@@ -80,11 +77,9 @@ describe("resolveMatrixRoomId", () => {
const client = {
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
getJoinedRoomMembers: vi.fn().mockResolvedValue([
"@bot:example.org",
userId,
"@extra:example.org",
]),
getJoinedRoomMembers: vi
.fn()
.mockResolvedValue(["@bot:example.org", userId, "@extra:example.org"]),
setAccountData: vi.fn().mockResolvedValue(undefined),
} as unknown as MatrixClient;

View File

@@ -31,8 +31,7 @@ async function persistDirectRoom(
} catch {
// Ignore fetch errors and fall back to an empty map.
}
const existing =
directContent && !Array.isArray(directContent) ? directContent : {};
const existing = directContent && !Array.isArray(directContent) ? directContent : {};
const current = Array.isArray(existing[userId]) ? existing[userId] : [];
if (current[0] === roomId) return;
const next = [roomId, ...current.filter((id) => id !== roomId)];
@@ -46,15 +45,10 @@ async function persistDirectRoom(
}
}
async function resolveDirectRoomId(
client: MatrixClient,
userId: string,
): Promise<string> {
async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
const trimmed = userId.trim();
if (!trimmed.startsWith("@")) {
throw new Error(
`Matrix user IDs must be fully qualified (got "${trimmed}")`,
);
throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
}
const cached = directRoomCache.get(trimmed);
@@ -65,9 +59,7 @@ async function resolveDirectRoomId(
const directContent = (await client.getAccountData(
EventType.Direct,
)) as MatrixDirectAccountData | null;
const list = Array.isArray(directContent?.[trimmed])
? directContent[trimmed]
: [];
const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : [];
if (list.length > 0) {
directRoomCache.set(trimmed, list[0]);
return list[0];
@@ -112,10 +104,7 @@ async function resolveDirectRoomId(
throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
}
export async function resolveMatrixRoomId(
client: MatrixClient,
raw: string,
): Promise<string> {
export async function resolveMatrixRoomId(client: MatrixClient, raw: string): Promise<string> {
const target = normalizeTarget(raw);
const lowered = target.toLowerCase();
if (lowered.startsWith("matrix:")) {

View File

@@ -15,7 +15,8 @@ import type { CoreConfig, DmPolicy } from "./types.js";
const channel = "matrix" as const;
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
const allowFrom = policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
const allowFrom =
policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -390,10 +391,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
unresolved.push(entry);
}
}
roomKeys = [
...resolvedIds,
...unresolved.map((entry) => entry.trim()).filter(Boolean),
];
roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
if (resolvedIds.length > 0 || unresolved.length > 0) {
await prompter.note(
[

View File

@@ -5,10 +5,7 @@ import type {
RuntimeEnv,
} from "openclaw/plugin-sdk";
import {
listMatrixDirectoryGroupsLive,
listMatrixDirectoryPeersLive,
} from "./directory-live.js";
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
function pickBestGroupMatch(
matches: ChannelDirectoryEntry[],

View File

@@ -76,7 +76,8 @@ export async function handleMatrixAction(
allowEmpty: true,
});
const mediaUrl = readStringParam(params, "mediaUrl");
const replyToId = readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
const replyToId =
readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const result = await sendMatrixMessage(to, content, {
mediaUrl: mediaUrl ?? undefined,

View File

@@ -1,8 +1,6 @@
{
"id": "mattermost",
"channels": [
"mattermost"
],
"channels": ["mattermost"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/mattermost",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Mattermost channel plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -12,10 +12,7 @@ import {
import { MattermostConfigSchema } from "./config-schema.js";
import { resolveMattermostGroupRequireMention } from "./group-mentions.js";
import {
looksLikeMattermostTargetId,
normalizeMattermostMessagingTarget,
} from "./normalize.js";
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
import { mattermostOnboardingAdapter } from "./onboarding.js";
import {
listMattermostAccountIds,
@@ -116,9 +113,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => formatAllowEntry(String(entry)))
.filter(Boolean),
allowFrom.map((entry) => formatAllowEntry(String(entry))).filter(Boolean),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {

View File

@@ -154,9 +154,7 @@ function normalizeAllowEntry(entry: string): string {
}
function normalizeAllowList(entries: Array<string | number>): string[] {
const normalized = entries
.map((entry) => normalizeAllowEntry(String(entry)))
.filter(Boolean);
const normalized = entries.map((entry) => normalizeAllowEntry(String(entry))).filter(Boolean);
return Array.from(new Set(normalized));
}
@@ -427,7 +425,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
hasControlCommand,
});
const commandAuthorized =
kind === "dm" ? dmPolicy === "open" || senderAllowedForCommands : commandGate.commandAuthorized;
kind === "dm"
? dmPolicy === "open" || senderAllowedForCommands
: commandGate.commandAuthorized;
if (kind === "dm") {
if (dmPolicy === "disabled") {
@@ -441,9 +441,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
id: senderId,
meta: { name: senderName },
});
logVerboseMessage(
`mattermost: pairing request sender=${senderId} created=${created}`,
);
logVerboseMessage(`mattermost: pairing request sender=${senderId} created=${created}`);
if (created) {
try {
await sendMessageMattermost(
@@ -457,15 +455,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
);
opts.statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
logVerboseMessage(
`mattermost: pairing reply failed for ${senderId}: ${String(err)}`,
);
logVerboseMessage(`mattermost: pairing reply failed for ${senderId}: ${String(err)}`);
}
}
} else {
logVerboseMessage(
`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`,
);
logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`);
}
return;
}
@@ -480,9 +474,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
return;
}
if (!groupAllowedForCommands) {
logVerboseMessage(
`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`,
);
logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`);
return;
}
}
@@ -542,14 +534,15 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
historyMap: channelHistories,
limit: historyLimit,
historyKey: historyKey ?? "",
entry: historyKey && trimmed
? {
sender: pendingSender,
body: trimmed,
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
messageId: post.id ?? undefined,
}
: null,
entry:
historyKey && trimmed
? {
sender: pendingSender,
body: trimmed,
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
messageId: post.id ?? undefined,
}
: null,
});
};
@@ -707,9 +700,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
`mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`,
);
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, {
fallbackLimit: account.textChunkLimit ?? 4000,
});
const textLimit = core.channel.text.resolveTextChunkLimit(
cfg,
"mattermost",
account.accountId,
{
fallbackLimit: account.textChunkLimit ?? 4000,
},
);
const tableMode = core.channel.text.resolveMarkdownTableMode({
cfg,
channel: "mattermost",
@@ -784,7 +782,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
markDispatchIdle();
if (historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: channelHistories, historyKey, limit: historyLimit });
clearHistoryEntriesIfEnabled({
historyMap: channelHistories,
historyKey,
limit: historyLimit,
});
}
};

View File

@@ -1,17 +1,17 @@
{
"name": "@openclaw/memory-core",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw core memory search plugin",
"openclaw": {
"extensions": [
"./index.ts"
]
"type": "module",
"devDependencies": {
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.1.26"
},
"devDependencies": {
"openclaw": "workspace:*"
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -48,11 +48,7 @@ const EMBEDDING_DIMENSIONS: Record<string, number> = {
"text-embedding-3-large": 3072,
};
function assertAllowedKeys(
value: Record<string, unknown>,
allowed: string[],
label: string,
) {
function assertAllowedKeys(value: Record<string, unknown>, allowed: string[], label: string) {
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length === 0) return;
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);

View File

@@ -83,9 +83,7 @@ class MemoryDB {
}
}
async store(
entry: Omit<MemoryEntry, "id" | "createdAt">,
): Promise<MemoryEntry> {
async store(entry: Omit<MemoryEntry, "id" | "createdAt">): Promise<MemoryEntry> {
await this.ensureInitialized();
const fullEntry: MemoryEntry = {
@@ -98,11 +96,7 @@ class MemoryDB {
return fullEntry;
}
async search(
vector: number[],
limit = 5,
minScore = 0.5,
): Promise<MemorySearchResult[]> {
async search(vector: number[], limit = 5, minScore = 0.5): Promise<MemorySearchResult[]> {
await this.ensureInitialized();
const results = await this.table!.vectorSearch(vector).limit(limit).toArray();
@@ -131,8 +125,7 @@ class MemoryDB {
async delete(id: string): Promise<boolean> {
await this.ensureInitialized();
// Validate UUID format to prevent injection
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(id)) {
throw new Error(`Invalid memory ID format: ${id}`);
}
@@ -203,8 +196,7 @@ function detectCategory(text: string): MemoryCategory {
const lower = text.toLowerCase();
if (/prefer|radši|like|love|hate|want/i.test(lower)) return "preference";
if (/rozhodli|decided|will use|budeme/i.test(lower)) return "decision";
if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower))
return "entity";
if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower)) return "entity";
if (/is|are|has|have|je|má|jsou/i.test(lower)) return "fact";
return "other";
}
@@ -227,9 +219,7 @@ const memoryPlugin = {
const db = new MemoryDB(resolvedDbPath, vectorDim);
const embeddings = new Embeddings(cfg.embedding.apiKey, cfg.embedding.model!);
api.logger.info(
`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`,
);
api.logger.info(`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`);
// ========================================================================
// Tools
@@ -275,9 +265,7 @@ const memoryPlugin = {
}));
return {
content: [
{ type: "text", text: `Found ${results.length} memories:\n\n${text}` },
],
content: [{ type: "text", text: `Found ${results.length} memories:\n\n${text}` }],
details: { count: results.length, memories: sanitizedResults },
};
},
@@ -293,9 +281,7 @@ const memoryPlugin = {
"Save important information in long-term memory. Use for preferences, facts, decisions.",
parameters: Type.Object({
text: Type.String({ description: "Information to remember" }),
importance: Type.Optional(
Type.Number({ description: "Importance 0-1 (default: 0.7)" }),
),
importance: Type.Optional(Type.Number({ description: "Importance 0-1 (default: 0.7)" })),
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
}),
async execute(_toolCallId, params) {
@@ -316,9 +302,16 @@ const memoryPlugin = {
if (existing.length > 0) {
return {
content: [
{ type: "text", text: `Similar memory already exists: "${existing[0].entry.text}"` },
{
type: "text",
text: `Similar memory already exists: "${existing[0].entry.text}"`,
},
],
details: { action: "duplicate", existingId: existing[0].entry.id, existingText: existing[0].entry.text },
details: {
action: "duplicate",
existingId: existing[0].entry.id,
existingText: existing[0].entry.text,
},
};
}
@@ -372,9 +365,7 @@ const memoryPlugin = {
if (results.length === 1 && results[0].score > 0.9) {
await db.delete(results[0].entry.id);
return {
content: [
{ type: "text", text: `Forgotten: "${results[0].entry.text}"` },
],
content: [{ type: "text", text: `Forgotten: "${results[0].entry.text}"` }],
details: { action: "deleted", id: results[0].entry.id },
};
}
@@ -417,9 +408,7 @@ const memoryPlugin = {
api.registerCli(
({ program }) => {
const memory = program
.command("ltm")
.description("LanceDB memory plugin commands");
const memory = program.command("ltm").description("LanceDB memory plugin commands");
memory
.command("list")
@@ -478,9 +467,7 @@ const memoryPlugin = {
.map((r) => `- [${r.entry.category}] ${r.entry.text}`)
.join("\n");
api.logger.info?.(
`memory-lancedb: injecting ${results.length} memories into context`,
);
api.logger.info?.(`memory-lancedb: injecting ${results.length} memories into context`);
return {
prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
@@ -536,9 +523,7 @@ const memoryPlugin = {
}
// Filter for capturable content
const toCapture = texts.filter(
(text) => text && shouldCapture(text),
);
const toCapture = texts.filter((text) => text && shouldCapture(text));
if (toCapture.length === 0) return;
// Store each capturable piece (limit to 3 per conversation)

View File

@@ -40,15 +40,10 @@
},
"model": {
"type": "string",
"enum": [
"text-embedding-3-small",
"text-embedding-3-large"
]
"enum": ["text-embedding-3-small", "text-embedding-3-large"]
}
},
"required": [
"apiKey"
]
"required": ["apiKey"]
},
"dbPath": {
"type": "string"
@@ -60,8 +55,6 @@
"type": "boolean"
}
},
"required": [
"embedding"
]
"required": ["embedding"]
}
}

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
"type": "module",
"dependencies": {
"@lancedb/lancedb": "^0.23.0",
"@sinclair/typebox": "0.34.48",

View File

@@ -19,7 +19,11 @@ function modelRef(modelId: string): string {
return `${PROVIDER_ID}/${modelId}`;
}
function buildModelDefinition(params: { id: string; name: string; input: Array<"text" | "image"> }) {
function buildModelDefinition(params: {
id: string;
name: string;
input: Array<"text" | "image">;
}) {
return {
id: params.id,
name: params.name,

View File

@@ -98,9 +98,7 @@ async function requestOAuthCode(params: {
);
}
if (payload.state !== params.state) {
throw new Error(
"MiniMax OAuth state mismatch: possible CSRF attack or session corruption.",
);
throw new Error("MiniMax OAuth state mismatch: possible CSRF attack or session corruption.");
}
return payload;
}
@@ -144,9 +142,7 @@ async function pollOAuthToken(params: {
return {
status: "error",
message:
payload?.base_resp?.status_msg ??
text ||
"MiniMax OAuth failed to parse response.",
(payload?.base_resp?.status_msg ?? text) || "MiniMax OAuth failed to parse response.",
};
}
@@ -165,9 +161,9 @@ async function pollOAuthToken(params: {
};
if (tokenPayload.status === "error") {
return { status: "error", message: "An error occurred. Please try again later"};
return { status: "error", message: "An error occurred. Please try again later" };
}
if (tokenPayload.status != "success") {
return { status: "pending", message: "current user code is not authorized" };
}
@@ -215,7 +211,6 @@ export async function loginMiniMaxPortalOAuth(params: {
let pollIntervalMs = oauth.interval ? oauth.interval : 2000;
const expireTimeMs = oauth.expired_in;
while (Date.now() < expireTimeMs) {
params.progress.update("Waiting for MiniMax OAuth approval…");
const result = await pollOAuthToken({

View File

@@ -1,8 +1,6 @@
{
"id": "minimax-portal-auth",
"providers": [
"minimax-portal"
],
"providers": ["minimax-portal"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,8 @@
{
"name": "@openclaw/minimax-portal-auth",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"

View File

@@ -3,51 +3,61 @@
## 2026.1.30
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.29
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.23
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.22
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.21
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.20
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.17-1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.17
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.1.15
### Features
- Bot Framework gateway monitor (Express + JWT auth) with configurable webhook path/port and `/api/messages` fallback.
- Onboarding flow for Azure Bot credentials (config + env var detection) and DM policy setup.
- Channel capabilities: DMs, group chats, channels, threads, media, polls, and `teams` alias.

View File

@@ -1,8 +1,6 @@
{
"id": "msteams",
"channels": [
"msteams"
],
"channels": ["msteams"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -1,8 +1,16 @@
{
"name": "@openclaw/msteams",
"version": "2026.1.30",
"type": "module",
"description": "OpenClaw Microsoft Teams channel plugin",
"type": "module",
"dependencies": {
"@microsoft/agents-hosting": "^1.2.3",
"@microsoft/agents-hosting-express": "^1.2.3",
"@microsoft/agents-hosting-extensions-teams": "^1.2.3",
"express": "^5.2.1",
"openclaw": "workspace:*",
"proper-lockfile": "^4.1.2"
},
"openclaw": {
"extensions": [
"./index.ts"
@@ -24,13 +32,5 @@
"localPath": "extensions/msteams",
"defaultChoice": "npm"
}
},
"dependencies": {
"@microsoft/agents-hosting": "^1.2.3",
"@microsoft/agents-hosting-express": "^1.2.3",
"@microsoft/agents-hosting-extensions-teams": "^1.2.3",
"express": "^5.2.1",
"openclaw": "workspace:*",
"proper-lockfile": "^4.1.2"
}
}

Some files were not shown because too many files have changed in this diff Show More