Compare commits

..
36 changed files with 33 additions and 284 deletions

No files matched your search

-2
View File
@@ -5,9 +5,7 @@ Docs: https://docs.clawd.bot
## 2026.1.22
### Fixes
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
## 2026.1.21-2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/bluebubbles",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot BlueBubbles channel plugin",
"clawdbot": {
@@ -1563,100 +1563,6 @@ describe("BlueBubbles webhook monitor", () => {
expect.any(Object),
);
});
it("stops typing on idle", async () => {
const { sendBlueBubblesTyping } = await import("./chat.js");
vi.mocked(sendBlueBubblesTyping).mockClear();
const account = createMockAccount();
const config: ClawdbotConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
chatGuid: "iMessage;-;+15551234567",
date: Date.now(),
},
};
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
await params.dispatcherOptions.onReplyStart?.();
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
await params.dispatcherOptions.onIdle?.();
});
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
expect.any(String),
false,
expect.any(Object),
);
});
it("stops typing when no reply is sent", async () => {
const { sendBlueBubblesTyping } = await import("./chat.js");
vi.mocked(sendBlueBubblesTyping).mockClear();
const account = createMockAccount();
const config: ClawdbotConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
chatGuid: "iMessage;-;+15551234567",
date: Date.now(),
},
};
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async () => undefined);
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
expect.any(String),
false,
expect.any(Object),
);
});
});
describe("outbound message ids", () => {
+3 -18
View File
@@ -1713,17 +1713,8 @@ async function processMessage(
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
}
},
onIdle: async () => {
if (!chatGuidForActions) return;
if (!baseUrl || !password) return;
try {
await sendBlueBubblesTyping(chatGuidForActions, false, {
cfg: config,
accountId: account.accountId,
});
} catch (err) {
logVerbose(core, runtime, `typing stop failed: ${String(err)}`);
}
onIdle: () => {
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
},
onError: (err, info) => {
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
@@ -1763,13 +1754,7 @@ async function processMessage(
});
}
if (chatGuidForActions && baseUrl && password && !sentMessage) {
// Stop typing indicator when no message was sent (e.g., NO_REPLY)
sendBlueBubblesTyping(chatGuidForActions, false, {
cfg: config,
accountId: account.accountId,
}).catch((err) => {
logVerbose(core, runtime, `typing stop (no reply) failed: ${String(err)}`);
});
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/copilot-proxy",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Copilot Proxy provider plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/diagnostics-otel",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot diagnostics OpenTelemetry exporter",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/discord",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Discord channel plugin",
"clawdbot": {
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/google-antigravity-auth",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Google Antigravity OAuth provider plugin",
"clawdbot": {
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/google-gemini-cli-auth",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Gemini CLI OAuth provider plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/imessage",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot iMessage channel plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/lobster",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"clawdbot": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/matrix",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Matrix channel plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/memory-core",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot core memory search plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/memory-lancedb",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
"dependencies": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/msteams",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Microsoft Teams channel plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/nextcloud-talk",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Nextcloud Talk channel plugin",
"clawdbot": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/nostr",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/signal",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Signal channel plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/slack",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Slack channel plugin",
"clawdbot": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/telegram",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Telegram channel plugin",
"clawdbot": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/voice-call",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot voice-call plugin",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/whatsapp",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot WhatsApp channel plugin",
"clawdbot": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/zalo",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Zalo channel plugin",
"clawdbot": {
-5
View File
@@ -1,10 +1,5 @@
# Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21
### Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clawdbot/zalouser",
"version": "2026.1.22",
"version": "2026.1.21",
"type": "module",
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawdbot",
"version": "2026.1.22",
"version": "2026.1.21-2",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module",
"main": "dist/index.js",
-66
View File
@@ -1,66 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveAssistantAvatarUrl } from "./control-ui.js";
describe("resolveAssistantAvatarUrl", () => {
it("keeps remote and data URLs", () => {
expect(
resolveAssistantAvatarUrl({
avatar: "https://example.com/avatar.png",
agentId: "main",
basePath: "/ui",
}),
).toBe("https://example.com/avatar.png");
expect(
resolveAssistantAvatarUrl({
avatar: "data:image/png;base64,abc",
agentId: "main",
basePath: "/ui",
}),
).toBe("data:image/png;base64,abc");
});
it("prefixes basePath for /avatar endpoints", () => {
expect(
resolveAssistantAvatarUrl({
avatar: "/avatar/main",
agentId: "main",
basePath: "/ui",
}),
).toBe("/ui/avatar/main");
expect(
resolveAssistantAvatarUrl({
avatar: "/ui/avatar/main",
agentId: "main",
basePath: "/ui",
}),
).toBe("/ui/avatar/main");
});
it("maps local avatar paths to the avatar endpoint", () => {
expect(
resolveAssistantAvatarUrl({
avatar: "avatars/me.png",
agentId: "main",
basePath: "/ui",
}),
).toBe("/ui/avatar/main");
expect(
resolveAssistantAvatarUrl({
avatar: "avatars/profile",
agentId: "main",
basePath: "/ui",
}),
).toBe("/ui/avatar/main");
});
it("keeps short text avatars", () => {
expect(
resolveAssistantAvatarUrl({
avatar: "PS",
agentId: "main",
basePath: "/ui",
}),
).toBe("PS");
});
});
+2 -40
View File
@@ -98,7 +98,7 @@ function sendJson(res: ServerResponse, status: number, body: unknown) {
res.end(JSON.stringify(body));
}
export function buildAvatarUrl(basePath: string, agentId: string): string {
function buildAvatarUrl(basePath: string, agentId: string): string {
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
}
@@ -206,49 +206,11 @@ interface ServeIndexHtmlOpts {
agentId?: string;
}
function looksLikeLocalAvatarPath(value: string): boolean {
if (/[\\/]/.test(value)) return true;
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
}
export function resolveAssistantAvatarUrl(params: {
avatar?: string | null;
agentId?: string | null;
basePath?: string;
}): string | undefined {
const avatar = params.avatar?.trim();
if (!avatar) return undefined;
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) return avatar;
const basePath = normalizeControlUiBasePath(params.basePath);
const baseAvatarPrefix = basePath ? `${basePath}${AVATAR_PREFIX}/` : `${AVATAR_PREFIX}/`;
if (basePath && avatar.startsWith(`${AVATAR_PREFIX}/`)) {
return `${basePath}${avatar}`;
}
if (avatar.startsWith(baseAvatarPrefix)) return avatar;
if (!params.agentId) return avatar;
if (looksLikeLocalAvatarPath(avatar)) {
return buildAvatarUrl(basePath, params.agentId);
}
return avatar;
}
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
const { basePath, config, agentId } = opts;
const identity = config
? resolveAssistantIdentity({ cfg: config, agentId })
: DEFAULT_ASSISTANT_IDENTITY;
const resolvedAgentId =
typeof (identity as { agentId?: string }).agentId === "string"
? (identity as { agentId?: string }).agentId
: agentId;
const avatarValue =
resolveAssistantAvatarUrl({
avatar: identity.avatar,
agentId: resolvedAgentId,
basePath,
}) ?? identity.avatar;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
const raw = fs.readFileSync(indexPath, "utf8");
@@ -256,7 +218,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex
injectControlUiConfig(raw, {
basePath,
assistantName: identity.name,
assistantAvatar: avatarValue,
assistantAvatar: identity.avatar,
}),
);
}
+1 -8
View File
@@ -38,7 +38,6 @@ import {
import { loadSessionEntry } from "../session-utils.js";
import { formatForLog } from "../ws-log.js";
import { resolveAssistantIdentity } from "../assistant-identity.js";
import { resolveAssistantAvatarUrl } from "../control-ui.js";
import { waitForAgentJob } from "./agent-job.js";
import type { GatewayRequestHandlers } from "./types.js";
@@ -408,13 +407,7 @@ export const agentHandlers: GatewayRequestHandlers = {
}
const cfg = loadConfig();
const identity = resolveAssistantIdentity({ cfg, agentId });
const avatarValue =
resolveAssistantAvatarUrl({
avatar: identity.avatar,
agentId: identity.agentId,
basePath: cfg.gateway?.controlUi?.basePath,
}) ?? identity.avatar;
respond(true, { ...identity, avatar: avatarValue }, undefined);
respond(true, identity, undefined);
},
"agent.wait": async ({ params, respond }) => {
if (!validateAgentWaitParams(params)) {
+4 -2
View File
@@ -160,9 +160,11 @@ const saveSessionToMemory: HookHandler = async (event) => {
await fs.writeFile(memoryFilePath, entry, "utf-8");
console.log("[session-memory] Memory file written successfully");
// Log completion (but don't send user-visible confirmation - it's internal housekeeping)
// Send confirmation message to user with filename
const relPath = memoryFilePath.replace(os.homedir(), "~");
console.log(`[session-memory] Session context saved to ${relPath}`);
const confirmMsg = `💾 Session context saved to memory before reset.\n📄 ${relPath}`;
event.messages.push(confirmMsg);
console.log("[session-memory] Confirmation message queued:", confirmMsg);
} catch (err) {
console.error(
"[session-memory] Failed to save session memory:",
+1 -2
View File
@@ -158,8 +158,7 @@ function renderAvatar(
function isAvatarUrl(value: string): boolean {
return (
/^https?:\/\//i.test(value) ||
/^data:image\//i.test(value) ||
/^\//.test(value) // Relative paths from avatar endpoint
/^data:image\//i.test(value)
);
}