mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 03:47:26 +00:00
chore: Run pnpm format:fix.
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "bluebubbles",
|
||||
"channels": [
|
||||
"bluebubbles"
|
||||
],
|
||||
"channels": ["bluebubbles"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.1.30",
|
||||
"type": "module",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}` };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "copilot-proxy",
|
||||
"providers": [
|
||||
"copilot-proxy"
|
||||
],
|
||||
"providers": ["copilot-proxy"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "discord",
|
||||
"channels": [
|
||||
"discord"
|
||||
],
|
||||
"channels": ["discord"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.1.30",
|
||||
"type": "module",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" };
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "google-antigravity-auth",
|
||||
"providers": [
|
||||
"google-antigravity"
|
||||
],
|
||||
"providers": ["google-antigravity"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "google-gemini-cli-auth",
|
||||
"providers": [
|
||||
"google-gemini-cli"
|
||||
],
|
||||
"providers": ["google-gemini-cli"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "googlechat",
|
||||
"channels": [
|
||||
"googlechat"
|
||||
],
|
||||
"channels": ["googlechat"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>", () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "imessage",
|
||||
"channels": [
|
||||
"imessage"
|
||||
],
|
||||
"channels": ["imessage"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/imessage",
|
||||
"version": "2026.1.30",
|
||||
"type": "module",
|
||||
"description": "OpenClaw iMessage channel plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "line",
|
||||
"channels": [
|
||||
"line"
|
||||
],
|
||||
"channels": ["line"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "matrix",
|
||||
"channels": [
|
||||
"matrix"
|
||||
],
|
||||
"channels": ["matrix"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: "*",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:")) {
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "mattermost",
|
||||
"channels": [
|
||||
"mattermost"
|
||||
],
|
||||
"channels": ["mattermost"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/mattermost",
|
||||
"version": "2026.1.30",
|
||||
"type": "module",
|
||||
"description": "OpenClaw Mattermost channel plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(", ")}`);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "minimax-portal-auth",
|
||||
"providers": [
|
||||
"minimax-portal"
|
||||
],
|
||||
"providers": ["minimax-portal"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "msteams",
|
||||
"channels": [
|
||||
"msteams"
|
||||
],
|
||||
"channels": ["msteams"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user