From 68824c8903ea5cbe429ee36bf63df6aa63b7c6c0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 20:58:35 +0000 Subject: [PATCH 01/34] chore: start 2026.1.25 changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eda32488..1c05e8691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Docs: https://docs.clawd.bot +## 2026.1.25 + +### Changes +- TBD. + ## 2026.1.24-3 ### Fixes From ffaeee4c39cbb56be473e719ded16e6b6b8d8986 Mon Sep 17 00:00:00 2001 From: Ross Morsali Date: Sun, 25 Jan 2026 19:56:04 +0100 Subject: [PATCH 02/34] fix: preserve CLI session IDs for session resume - Add resumeArgs to DEFAULT_CLAUDE_BACKEND for proper --resume flag usage - Fix gateway not preserving cliSessionIds/claudeCliSessionId in nextEntry - Add test for CLI session ID preservation in gateway agent handler - Update docs with new resumeArgs default --- docs/gateway/cli-backends.md | 1 + src/agents/cli-backends.ts | 8 ++ src/gateway/server-methods/agent.test.ts | 163 +++++++++++++++++++++++ src/gateway/server-methods/agent.ts | 2 + 4 files changed, 174 insertions(+) create mode 100644 src/gateway/server-methods/agent.test.ts diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index 917145cc2..092533c2e 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -182,6 +182,7 @@ Clawdbot ships a default for `claude-cli`: - `command: "claude"` - `args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"]` +- `resumeArgs: ["-p", "--output-format", "json", "--dangerously-skip-permissions", "--resume", "{sessionId}"]` - `modelArg: "--model"` - `systemPromptArg: "--append-system-prompt"` - `sessionArg: "--session-id"` diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index a2fcaa8a5..f21c04f52 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -28,6 +28,14 @@ const CLAUDE_MODEL_ALIASES: Record = { const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { command: "claude", args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"], + resumeArgs: [ + "-p", + "--output-format", + "json", + "--dangerously-skip-permissions", + "--resume", + "{sessionId}", + ], output: "json", input: "arg", modelArg: "--model", diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts new file mode 100644 index 000000000..149ab4a67 --- /dev/null +++ b/src/gateway/server-methods/agent.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { GatewayRequestContext } from "./types.js"; +import { agentHandlers } from "./agent.js"; + +const mocks = vi.hoisted(() => ({ + loadSessionEntry: vi.fn(), + updateSessionStore: vi.fn(), + agentCommand: vi.fn(), + registerAgentRunContext: vi.fn(), +})); + +vi.mock("../session-utils.js", () => ({ + loadSessionEntry: mocks.loadSessionEntry, +})); + +vi.mock("../../config/sessions.js", async () => { + const actual = await vi.importActual( + "../../config/sessions.js", + ); + return { + ...actual, + updateSessionStore: mocks.updateSessionStore, + resolveAgentIdFromSessionKey: () => "main", + resolveExplicitAgentSessionKey: () => undefined, + resolveAgentMainSessionKey: () => "agent:main:main", + }; +}); + +vi.mock("../../commands/agent.js", () => ({ + agentCommand: mocks.agentCommand, +})); + +vi.mock("../../config/config.js", () => ({ + loadConfig: () => ({}), +})); + +vi.mock("../../agents/agent-scope.js", () => ({ + listAgentIds: () => ["main"], +})); + +vi.mock("../../infra/agent-events.js", () => ({ + registerAgentRunContext: mocks.registerAgentRunContext, + onAgentEvent: vi.fn(), +})); + +vi.mock("../../sessions/send-policy.js", () => ({ + resolveSendPolicy: () => "allow", +})); + +vi.mock("../../utils/delivery-context.js", async () => { + const actual = await vi.importActual( + "../../utils/delivery-context.js", + ); + return { + ...actual, + normalizeSessionDeliveryFields: () => ({}), + }; +}); + +const makeContext = (): GatewayRequestContext => + ({ + dedupe: new Map(), + addChatRun: vi.fn(), + logGateway: { info: vi.fn(), error: vi.fn() }, + }) as unknown as GatewayRequestContext; + +describe("gateway agent handler", () => { + it("preserves cliSessionIds from existing session entry", async () => { + const existingCliSessionIds = { "claude-cli": "abc-123-def" }; + const existingClaudeCliSessionId = "abc-123-def"; + + mocks.loadSessionEntry.mockReturnValue({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "existing-session-id", + updatedAt: Date.now(), + cliSessionIds: existingCliSessionIds, + claudeCliSessionId: existingClaudeCliSessionId, + }, + canonicalKey: "agent:main:main", + }); + + let capturedEntry: Record | undefined; + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const store: Record = {}; + await updater(store); + capturedEntry = store["agent:main:main"] as Record; + }); + + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + const respond = vi.fn(); + await agentHandlers.agent({ + params: { + message: "test", + agentId: "main", + sessionKey: "agent:main:main", + idempotencyKey: "test-idem", + }, + respond, + context: makeContext(), + req: { type: "req", id: "1", method: "agent" }, + client: null, + isWebchatConnect: () => false, + }); + + expect(mocks.updateSessionStore).toHaveBeenCalled(); + expect(capturedEntry).toBeDefined(); + expect(capturedEntry?.cliSessionIds).toEqual(existingCliSessionIds); + expect(capturedEntry?.claudeCliSessionId).toBe(existingClaudeCliSessionId); + }); + + it("handles missing cliSessionIds gracefully", async () => { + mocks.loadSessionEntry.mockReturnValue({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "existing-session-id", + updatedAt: Date.now(), + // No cliSessionIds or claudeCliSessionId + }, + canonicalKey: "agent:main:main", + }); + + let capturedEntry: Record | undefined; + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const store: Record = {}; + await updater(store); + capturedEntry = store["agent:main:main"] as Record; + }); + + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + const respond = vi.fn(); + await agentHandlers.agent({ + params: { + message: "test", + agentId: "main", + sessionKey: "agent:main:main", + idempotencyKey: "test-idem-2", + }, + respond, + context: makeContext(), + req: { type: "req", id: "2", method: "agent" }, + client: null, + isWebchatConnect: () => false, + }); + + expect(mocks.updateSessionStore).toHaveBeenCalled(); + expect(capturedEntry).toBeDefined(); + // Should be undefined, not cause an error + expect(capturedEntry?.cliSessionIds).toBeUndefined(); + expect(capturedEntry?.claudeCliSessionId).toBeUndefined(); + }); +}); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 8c5782e00..d159d1f78 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -251,6 +251,8 @@ export const agentHandlers: GatewayRequestHandlers = { groupId: resolvedGroupId ?? entry?.groupId, groupChannel: resolvedGroupChannel ?? entry?.groupChannel, space: resolvedGroupSpace ?? entry?.space, + cliSessionIds: entry?.cliSessionIds, + claudeCliSessionId: entry?.claudeCliSessionId, }; sessionEntry = nextEntry; const sendPolicy = resolveSendPolicy({ From ae030c32dacdaafeb58147571a1c2e9dbc1d5c03 Mon Sep 17 00:00:00 2001 From: Ross Morsali Date: Sun, 25 Jan 2026 20:11:57 +0100 Subject: [PATCH 03/34] fix: emit assistant event for CLI backend responses in TUI CLI backends (claude-cli etc) don't emit streaming assistant events, causing TUI to show "(no output)" despite correct processing. Now emits assistant event with final text before lifecycle end so server-chat buffer gets populated for WebSocket clients. --- src/auto-reply/reply/agent-runner-execution.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index a428aa6da..47c45b09d 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -179,6 +179,17 @@ export async function runAgentTurnWithFallback(params: { images: params.opts?.images, }) .then((result) => { + // CLI backends don't emit streaming assistant events, so we need to + // emit one with the final text so server-chat can populate its buffer + // and send the response to TUI/WebSocket clients. + const cliText = result.payloads?.[0]?.text?.trim(); + if (cliText) { + emitAgentEvent({ + runId, + stream: "assistant", + data: { text: cliText }, + }); + } emitAgentEvent({ runId, stream: "lifecycle", From 6ffc5d93e4d14a7b6dc7cc17187f332b9f143823 Mon Sep 17 00:00:00 2001 From: Ross Morsali Date: Sun, 25 Jan 2026 21:12:45 +0100 Subject: [PATCH 04/34] test: update CLI runner test to expect --resume for session resume --- src/agents/claude-cli-runner.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/claude-cli-runner.test.ts b/src/agents/claude-cli-runner.test.ts index 6414aecb5..7825d00da 100644 --- a/src/agents/claude-cli-runner.test.ts +++ b/src/agents/claude-cli-runner.test.ts @@ -61,7 +61,7 @@ describe("runClaudeCliAgent", () => { expect(argv).toContain("hi"); }); - it("uses provided --session-id when a claude session id is provided", async () => { + it("uses --resume when a claude session id is provided", async () => { runCommandWithTimeoutMock.mockResolvedValueOnce({ stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }), stderr: "", @@ -83,7 +83,7 @@ describe("runClaudeCliAgent", () => { expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1); const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[]; - expect(argv).toContain("--session-id"); + expect(argv).toContain("--resume"); expect(argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b"); expect(argv).toContain("hi"); }); From e0adf65dac311a6ab253de5f915ef77455b3026f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 21:08:23 +0000 Subject: [PATCH 05/34] test: cover CLI chat delta event (#1921) (thanks @rmorse) --- CHANGELOG.md | 1 + src/gateway/server-chat.agent-events.test.ts | 43 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/gateway/server-chat.agent-events.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c05e8691..ee138f13e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.clawd.bot ### Fixes - Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. +- CLI: resume claude-cli sessions and stream CLI replies to TUI clients. (#1921) Thanks @rmorse. ## 2026.1.24-2 diff --git a/src/gateway/server-chat.agent-events.test.ts b/src/gateway/server-chat.agent-events.test.ts new file mode 100644 index 000000000..14657464a --- /dev/null +++ b/src/gateway/server-chat.agent-events.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createAgentEventHandler, createChatRunState } from "./server-chat.js"; + +describe("agent event handler", () => { + it("emits chat delta for assistant text-only events", () => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_000); + const broadcast = vi.fn(); + const nodeSendToSession = vi.fn(); + const agentRunSeq = new Map(); + const chatRunState = createChatRunState(); + chatRunState.registry.add("run-1", { sessionKey: "session-1", clientRunId: "client-1" }); + + const handler = createAgentEventHandler({ + broadcast, + nodeSendToSession, + agentRunSeq, + chatRunState, + resolveSessionKeyForRun: () => undefined, + clearAgentRunContext: vi.fn(), + }); + + handler({ + runId: "run-1", + seq: 1, + stream: "assistant", + ts: Date.now(), + data: { text: "Hello world" }, + }); + + const chatCalls = broadcast.mock.calls.filter(([event]) => event === "chat"); + expect(chatCalls).toHaveLength(1); + const payload = chatCalls[0]?.[1] as { + state?: string; + message?: { content?: Array<{ text?: string }> }; + }; + expect(payload.state).toBe("delta"); + expect(payload.message?.content?.[0]?.text).toBe("Hello world"); + const sessionChatCalls = nodeSendToSession.mock.calls.filter(([, event]) => event === "chat"); + expect(sessionChatCalls).toHaveLength(1); + nowSpy.mockRestore(); + }); +}); From 50b4126c79536a9645cddcfe6801916b5f6d9343 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Sun, 25 Jan 2026 13:42:56 -0800 Subject: [PATCH 06/34] Update deployment link for Railway template --- docs/railway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/railway.mdx b/docs/railway.mdx index 808416f50..b8f994a7d 100644 --- a/docs/railway.mdx +++ b/docs/railway.mdx @@ -16,7 +16,7 @@ and you configure everything via the `/setup` web wizard. ## One-click deploy -Deploy on Railway +Deploy on Railway After deploy, find your public URL in **Railway → your service → Settings → Domains**. From 8f6542409a57c99952c4f03323f52498bc958399 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 22:13:00 +0000 Subject: [PATCH 07/34] chore: bump versions for 2026.1.25 --- CHANGELOG.md | 1 + apps/android/app/build.gradle.kts | 4 ++-- apps/ios/Sources/Info.plist | 4 ++-- apps/ios/Tests/Info.plist | 4 ++-- apps/ios/project.yml | 8 ++++---- apps/macos/Sources/Clawdbot/Resources/Info.plist | 4 ++-- docs/platforms/fly.md | 2 +- docs/platforms/mac/release.md | 14 +++++++------- docs/reference/RELEASING.md | 2 +- extensions/bluebubbles/package.json | 2 +- extensions/copilot-proxy/package.json | 2 +- extensions/diagnostics-otel/package.json | 2 +- extensions/discord/package.json | 2 +- extensions/google-antigravity-auth/package.json | 2 +- extensions/google-gemini-cli-auth/package.json | 2 +- extensions/googlechat/package.json | 4 ++-- extensions/imessage/package.json | 2 +- extensions/line/package.json | 2 +- extensions/llm-task/package.json | 2 +- extensions/lobster/package.json | 2 +- extensions/matrix/package.json | 2 +- extensions/mattermost/package.json | 2 +- extensions/memory-core/package.json | 4 ++-- extensions/memory-lancedb/package.json | 2 +- extensions/msteams/package.json | 2 +- extensions/nextcloud-talk/package.json | 2 +- extensions/nostr/package.json | 2 +- extensions/open-prose/package.json | 2 +- extensions/signal/package.json | 2 +- extensions/slack/package.json | 2 +- extensions/telegram/package.json | 2 +- extensions/tlon/package.json | 2 +- extensions/voice-call/CHANGELOG.md | 2 +- extensions/voice-call/package.json | 2 +- extensions/whatsapp/package.json | 2 +- extensions/zalo/package.json | 2 +- extensions/zalouser/package.json | 2 +- package.json | 4 ++-- 38 files changed, 54 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee138f13e..afdbb8463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Docs: https://docs.clawd.bot ## 2026.1.25 +Status: unreleased. ### Changes - TBD. diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index d8d77ebe1..a015c0e36 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.clawdbot.android" minSdk = 31 targetSdk = 36 - versionCode = 202601240 - versionName = "2026.1.24" + versionCode = 202601250 + versionName = "2026.1.25" } buildTypes { diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 9dd7a0315..e1cf2b71d 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.24 + 2026.1.25 CFBundleVersion - 20260124 + 20260125 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 798a77421..6ff977b05 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2026.1.24 + 2026.1.25 CFBundleVersion - 20260124 + 20260125 diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 52faeb9d0..0073b4ef9 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -81,8 +81,8 @@ targets: properties: CFBundleDisplayName: Clawdbot CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.1.24" - CFBundleVersion: "20260124" + CFBundleShortVersionString: "2026.1.25" + CFBundleVersion: "20260125" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false @@ -130,5 +130,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: ClawdbotTests - CFBundleShortVersionString: "2026.1.24" - CFBundleVersion: "20260124" + CFBundleShortVersionString: "2026.1.25" + CFBundleVersion: "20260125" diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist index 1c7d9619f..ee9e3113d 100644 --- a/apps/macos/Sources/Clawdbot/Resources/Info.plist +++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.24 + 2026.1.25 CFBundleVersion - 202601240 + 202601250 CFBundleIconFile Clawdbot CFBundleURLTypes diff --git a/docs/platforms/fly.md b/docs/platforms/fly.md index d43b83ed7..0fdf176ae 100644 --- a/docs/platforms/fly.md +++ b/docs/platforms/fly.md @@ -182,7 +182,7 @@ cat > /data/clawdbot.json << 'EOF' "bind": "auto" }, "meta": { - "lastTouchedVersion": "2026.1.24" + "lastTouchedVersion": "2026.1.25" } } EOF diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md index d2d267661..d3bfd02c3 100644 --- a/docs/platforms/mac/release.md +++ b/docs/platforms/mac/release.md @@ -30,17 +30,17 @@ Notes: # From repo root; set release IDs so Sparkle feed is enabled. # APP_BUILD must be numeric + monotonic for Sparkle compare. BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.24-3 \ +APP_VERSION=2026.1.25 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-app.sh # Zip for distribution (includes resource forks for Sparkle delta support) -ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.24-3.zip +ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.25.zip # Optional: also build a styled DMG for humans (drag to /Applications) -scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24-3.dmg +scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg # Recommended: build + notarize/staple zip + DMG # First, create a keychain profile once: @@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24-3.dmg # --apple-id "" --team-id "" --password "" NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.24-3 \ +APP_VERSION=2026.1.25 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-dist.sh # Optional: ship dSYM alongside the release -ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.24-3.dSYM.zip +ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.25.dSYM.zip ``` ## Appcast entry Use the release note generator so Sparkle renders formatted HTML notes: ```bash -SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.24-3.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml +SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.25.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml ``` Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify -- Upload `Clawdbot-2026.1.24-3.zip` (and `Clawdbot-2026.1.24-3.dSYM.zip`) to the GitHub release for tag `v2026.1.24-3`. +- Upload `Clawdbot-2026.1.25.zip` (and `Clawdbot-2026.1.25.dSYM.zip`) to the GitHub release for tag `v2026.1.25`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`. - Sanity checks: - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 6492bd469..244757a48 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -17,7 +17,7 @@ When the operator says “release”, immediately do this preflight (no extra qu - Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed. 1) **Version & metadata** -- [ ] Bump `package.json` version (e.g., `2026.1.24`). +- [ ] Bump `package.json` version (e.g., `2026.1.25`). - [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs. - [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts). - [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`. diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json index 925b05bc1..7d82036a0 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/bluebubbles", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot BlueBubbles channel plugin", "clawdbot": { diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index 792a94225..2a9a63c71 100644 --- a/extensions/copilot-proxy/package.json +++ b/extensions/copilot-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/copilot-proxy", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Copilot Proxy provider plugin", "clawdbot": { diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 2afc99e2e..65a6bf0cd 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/diagnostics-otel", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot diagnostics OpenTelemetry exporter", "clawdbot": { diff --git a/extensions/discord/package.json b/extensions/discord/package.json index dae5fe1f1..90a99d4d3 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/discord", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Discord channel plugin", "clawdbot": { diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json index 96bffde7c..f1d8f86bd 100644 --- a/extensions/google-antigravity-auth/package.json +++ b/extensions/google-antigravity-auth/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/google-antigravity-auth", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Google Antigravity OAuth provider plugin", "clawdbot": { diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json index dc8a894d7..7e3fef15b 100644 --- a/extensions/google-gemini-cli-auth/package.json +++ b/extensions/google-gemini-cli-auth/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/google-gemini-cli-auth", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Gemini CLI OAuth provider plugin", "clawdbot": { diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index 056bdedb6..af1ccf8e1 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/googlechat", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Google Chat channel plugin", "clawdbot": { @@ -34,6 +34,6 @@ "clawdbot": "workspace:*" }, "peerDependencies": { - "clawdbot": ">=2026.1.24" + "clawdbot": ">=2026.1.25" } } diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 79aa7890d..944ad06bf 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/imessage", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot iMessage channel plugin", "clawdbot": { diff --git a/extensions/line/package.json b/extensions/line/package.json index b518f5ca5..346d66415 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/line", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot LINE channel plugin", "clawdbot": { diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json index a03344d1a..d6bfbb31d 100644 --- a/extensions/llm-task/package.json +++ b/extensions/llm-task/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/llm-task", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot JSON-only LLM task plugin", "clawdbot": { diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index 3926b553b..b73dbac69 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/lobster", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "clawdbot": { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 24529ee97..7fa12bc74 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/matrix", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Matrix channel plugin", "clawdbot": { diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 77d799c34..60c02d50f 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/mattermost", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Mattermost channel plugin", "clawdbot": { diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index c70c2a63f..c70da1395 100644 --- a/extensions/memory-core/package.json +++ b/extensions/memory-core/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/memory-core", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot core memory search plugin", "clawdbot": { @@ -9,6 +9,6 @@ ] }, "peerDependencies": { - "clawdbot": ">=2026.1.24" + "clawdbot": ">=2026.1.25" } } diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json index 80018044f..e003f5890 100644 --- a/extensions/memory-lancedb/package.json +++ b/extensions/memory-lancedb/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/memory-lancedb", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture", "dependencies": { diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index b336b80e6..b94f8e76a 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/msteams", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Microsoft Teams channel plugin", "clawdbot": { diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json index bf5e443e5..2da3f3b2a 100644 --- a/extensions/nextcloud-talk/package.json +++ b/extensions/nextcloud-talk/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/nextcloud-talk", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Nextcloud Talk channel plugin", "clawdbot": { diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index 3a3e5ac56..b2fb4b799 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/nostr", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs", "clawdbot": { diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json index 873f3458a..052201205 100644 --- a/extensions/open-prose/package.json +++ b/extensions/open-prose/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/open-prose", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "clawdbot": { diff --git a/extensions/signal/package.json b/extensions/signal/package.json index 034c65dea..65948eb7b 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/signal", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Signal channel plugin", "clawdbot": { diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 73f2f6ecd..5bd452d2e 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/slack", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Slack channel plugin", "clawdbot": { diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 81b378df2..64d3d7dea 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/telegram", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Telegram channel plugin", "clawdbot": { diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index dca4f914d..06750126d 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/tlon", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Tlon/Urbit channel plugin", "clawdbot": { diff --git a/extensions/voice-call/CHANGELOG.md b/extensions/voice-call/CHANGELOG.md index 6123a7315..a8721d47d 100644 --- a/extensions/voice-call/CHANGELOG.md +++ b/extensions/voice-call/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2026.1.24 +## 2026.1.25 ### Changes - Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deep‑merges with core). diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index 840776c19..31b171f76 100644 --- a/extensions/voice-call/package.json +++ b/extensions/voice-call/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/voice-call", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot voice-call plugin", "dependencies": { diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index 8e18af842..b7b57eb51 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/whatsapp", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot WhatsApp channel plugin", "clawdbot": { diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index a3a87a878..8f077a6b3 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/zalo", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Zalo channel plugin", "clawdbot": { diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 513295b46..0ab93d1ce 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { "name": "@clawdbot/zalouser", - "version": "2026.1.24", + "version": "2026.1.25", "type": "module", "description": "Clawdbot Zalo Personal Account plugin via zca-cli", "dependencies": { diff --git a/package.json b/package.json index 5d77e25d0..2a841139f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawdbot", - "version": "2026.1.24-3", + "version": "2026.1.25", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", @@ -220,7 +220,7 @@ "@types/proper-lockfile": "^4.1.4", "@types/qrcode-terminal": "^0.12.2", "@types/ws": "^8.18.1", - "@typescript/native-preview": "7.0.0-dev.20260124.1", + "@typescript/native-preview": "7.0.0-dev.20260125.1", "@vitest/coverage-v8": "^4.0.18", "docx-preview": "^0.3.7", "lit": "^3.3.2", From 5c231fc21f7d458edf2d766da336c817fb9796de Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:01:38 -0600 Subject: [PATCH 08/34] Doctor: warn on gateway exposure (#2016) Co-authored-by: Alex Alaniz --- CHANGELOG.md | 2 +- src/commands/doctor-security.ts | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afdbb8463..cacc265a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Docs: https://docs.clawd.bot Status: unreleased. ### Changes -- TBD. +- Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. ## 2026.1.24-3 diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index b3d82247f..483917faa 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -10,6 +10,61 @@ export async function noteSecurityWarnings(cfg: ClawdbotConfig) { const warnings: string[] = []; const auditHint = `- Run: ${formatCliCommand("clawdbot security audit --deep")}`; + // =========================================== + // GATEWAY NETWORK EXPOSURE CHECK + // =========================================== + // Check for dangerous gateway binding configurations + // that expose the gateway to network without proper auth + + const gatewayBind = cfg.gateway?.bind ?? "loopback"; + const customBindHost = cfg.gateway?.customBindHost?.trim(); + const authMode = cfg.gateway?.auth?.mode ?? "off"; + const authToken = cfg.gateway?.auth?.token; + const authPassword = cfg.gateway?.auth?.password; + + const isLoopbackBindHost = (host: string) => { + const normalized = host.trim().toLowerCase(); + return ( + normalized === "localhost" || + normalized === "::1" || + normalized === "[::1]" || + normalized.startsWith("127.") + ); + }; + + // Bindings that expose gateway beyond localhost + const exposedBindings = ["all", "lan", "0.0.0.0"]; + const isExposed = + exposedBindings.includes(gatewayBind) || + (gatewayBind === "custom" && (!customBindHost || !isLoopbackBindHost(customBindHost))); + + if (isExposed) { + if (authMode === "off") { + warnings.push( + `- CRITICAL: Gateway bound to "${gatewayBind}" with NO authentication.`, + ` Anyone on your network (or internet if port-forwarded) can fully control your agent.`, + ` Fix: ${formatCliCommand("clawdbot config set gateway.bind loopback")}`, + ` Or enable auth: ${formatCliCommand("clawdbot config set gateway.auth.mode token")}`, + ); + } else if (authMode === "token" && !authToken) { + warnings.push( + `- CRITICAL: Gateway bound to "${gatewayBind}" with empty auth token.`, + ` Fix: ${formatCliCommand("clawdbot doctor --fix")} to generate a token`, + ); + } else if (authMode === "password" && !authPassword) { + warnings.push( + `- CRITICAL: Gateway bound to "${gatewayBind}" with empty password.`, + ` Fix: ${formatCliCommand("clawdbot configure")} to set a password`, + ); + } else { + // Auth is configured, but still warn about network exposure + warnings.push( + `- WARNING: Gateway bound to "${gatewayBind}" (network-accessible).`, + ` Ensure your auth credentials are strong and not exposed.`, + ); + } + } + const warnDmPolicy = async (params: { label: string; provider: ChannelId; From 44bf454508322964c66f7c35f72fb935d8608617 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:02:28 -0600 Subject: [PATCH 09/34] Docs: update clawtributors --- README.md | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ebbdc43d5..47f3a9090 100644 --- a/README.md +++ b/README.md @@ -479,31 +479,32 @@ Thanks to all clawtributors:

steipete plum-dawg bohdanpodvirnyi iHildy joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg rahthakor vrknetha radek-paclt Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall xadenryan rodrigouroz - juanpablodlc hsrvc magimetal meaningfool tyler6204 patelhiren NicholasSpisak jonisjongithub abhisekbasu1 zerone0x - jamesgroat claude JustYannicc SocialNerd42069 Hyaxia dantelex daveonkels google-labs-jules[bot] lc0rp mousberg - vignesh07 mteam88 Eng. Juan Combetto Mariano Belinky dbhurley TSavo julianengel bradleypriest benithors rohannagpal - timolins benostein f-trycua nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino - Vasanth Rao Naik Sabavat petter-b cpojer scald gumadeiras andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet - peschee rafaelreis-r thewilloftheshadow ratulsarna lutr0 danielz1z emanuelst KristijanJovanovski CashWilliams rdev - osolmaz joshrad-dev kiranjd adityashaw2 sheeek artuskg Takhoffman onutc pauloportella neooriginal - manuelhettich minghinmatthewlam myfunc travisirby buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood - timkrase uos-status gerardward2007 obviyus roshanasingh4 tosh-hamburg azade-c bjesuiter cheeeee Josh Phillips - robbyczgw-cla dlauer pookNast Whoaa512 YuriNachos chriseidhof ngutman ysqander aj47 superman32432432 - Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman - jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 Ghost jonasjancarik - Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz Friederike Seiler gabriel-trigo - iamadig Jonathan D. Rhyne (DJ-D) Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff siddhantjain - suminhthanh svkozak VACInc wes-davis zats 24601 adam91holt ameno- Chris Taylor Django Navarro - evalexpr henrino3 humanwritten larlyssa odysseus0 oswalpalash pcty-nextgen-service-account Syhids Aaron Konyer aaronveklabs - andreabadesso Andrii cash-echo-bot Clawd ClawdFx dguido EnzeD erik-agens Evizero fcatuhe - itsjaydesu ivancasco ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior jverdi longmaba mickahouan mjrussell - odnxe p6l-richard philipp-spiess robaxelsen Sash Catanzarite T5-AndyML travisp VAC william arzt zknicker - abhaymundhara alejandro maza andrewting19 anpoirier arthyn Asleep123 bolismauro conhecendoia dasilva333 Developer - Dimitrios Ploutarchos Drake Thomsen fal3 Felix Krause ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna - Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin levifig Lloyd loukotal louzhixian martinpucik Matt mini - Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 prathamdby ptn1411 reeltimeapps RLTCmpe - Rolf Fredheim Rony Kelner Samrat Jha senoldogann Seredeep sergical shiv19 siraht snopoke testingabc321 - The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin Wimmie wstock yazinsai ymat19 Zach Knickerbocker - aaronn Alphonse-arianee Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik - pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock + juanpablodlc hsrvc magimetal meaningfool tyler6204 patelhiren NicholasSpisak jonisjongithub zerone0x abhisekbasu1 + jamesgroat claude JustYannicc Hyaxia dantelex SocialNerd42069 daveonkels google-labs-jules[bot] lc0rp mousberg + vignesh07 mteam88 dbhurley Mariano Belinky Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal + timolins f-trycua benostein nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino + Vasanth Rao Naik Sabavat petter-b cpojer scald gumadeiras andranik-sahakyan davidguttman sleontenko denysvitali orlyjamie + sircrumpet peschee rafaelreis-r thewilloftheshadow ratulsarna lutr0 danielz1z emanuelst KristijanJovanovski rdev + joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek artuskg Takhoffman onutc pauloportella + neooriginal manuelhettich minghinmatthewlam myfunc travisirby buddyh connorshea kyleok mcinteerj dependabot[bot] + John-Rood timkrase uos-status gerardward2007 obviyus roshanasingh4 tosh-hamburg azade-c JonUleis bjesuiter + cheeeee Josh Phillips robbyczgw-cla dlauer pookNast Whoaa512 YuriNachos chriseidhof ngutman ysqander + aj47 superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy + imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 + Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz + Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Kit koala73 manmal ogulcancelik pasogott petradonka + rubyrunsstuff siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 adam91holt ameno- + Chris Taylor Django Navarro evalexpr henrino3 humanwritten larlyssa odysseus0 oswalpalash pcty-nextgen-service-account rmorse + Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx dguido EnzeD + erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior jverdi + longmaba mickahouan mjrussell odnxe p6l-richard philipp-spiess robaxelsen Sash Catanzarite T5-AndyML travisp + VAC william arzt zknicker abhaymundhara alejandro maza andrewting19 anpoirier arthyn Asleep123 bolismauro + conhecendoia dasilva333 Developer Dimitrios Ploutarchos Drake Thomsen fal3 Felix Krause foeken ganghyun kim grrowl + gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin kitze levifig + Lloyd loukotal louzhixian martinpucik Matt mini Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman + nexty5870 Noctivoro prathamdby ptn1411 reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann + Seredeep sergical shiv19 shiyuanhai siraht snopoke testingabc321 The Admiral thesash Ubuntu + voidserf Vultr-Clawd Admin Wimmie wstock yazinsai ymat19 Zach Knickerbocker aaronn Alphonse-arianee Azade + carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres + rhjoh ronak-guliani William Stock

From 7ea4b06a046ad1bdf979941c605e0fbea81a664d Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:05:00 -0600 Subject: [PATCH 10/34] Deps: revert native-preview to published version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a841139f..0c63d5d69 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "@types/proper-lockfile": "^4.1.4", "@types/qrcode-terminal": "^0.12.2", "@types/ws": "^8.18.1", - "@typescript/native-preview": "7.0.0-dev.20260125.1", + "@typescript/native-preview": "7.0.0-dev.20260124.1", "@vitest/coverage-v8": "^4.0.18", "docx-preview": "^0.3.7", "lit": "^3.3.2", From 138916a0d1a20e613dd2db98239877244c5ad1e9 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:11:21 -0600 Subject: [PATCH 11/34] Deps: sync memory-core lockfile spec --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 781a461a9..14bef9f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,7 +357,7 @@ importers: extensions/memory-core: dependencies: clawdbot: - specifier: '>=2026.1.24' + specifier: '>=2026.1.25' version: link:../.. extensions/memory-lancedb: From 9c26cded75615cdd2683981a21633b4b6fb799fa Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:22:10 -0600 Subject: [PATCH 12/34] Docs: add Vercel AI Gateway sidebar entry (#1901) Co-authored-by: Jerilyn Zheng --- CHANGELOG.md | 1 + docs/docs.json | 1 + docs/providers/vercel-ai-gateway.md | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cacc265a3..5e4a7005d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Status: unreleased. ### Changes - Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. +- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. ## 2026.1.24-3 diff --git a/docs/docs.json b/docs/docs.json index 09b248990..4af7943e0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -983,6 +983,7 @@ "bedrock", "providers/moonshot", "providers/minimax", + "providers/vercel-ai-gateway", "providers/openrouter", "providers/synthetic", "providers/opencode", diff --git a/docs/providers/vercel-ai-gateway.md b/docs/providers/vercel-ai-gateway.md index bd31f0a87..36cf51cda 100644 --- a/docs/providers/vercel-ai-gateway.md +++ b/docs/providers/vercel-ai-gateway.md @@ -1,4 +1,5 @@ --- +title: "Vercel AI Gateway" summary: "Vercel AI Gateway setup (auth + model selection)" read_when: - You want to use Vercel AI Gateway with Clawdbot From c7fabb43f98e27c95fffc656dded87e7a9371355 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:23:40 -0600 Subject: [PATCH 13/34] Agents: expand cron tool description (#1988) Co-authored-by: Tomas Cupr --- CHANGELOG.md | 1 + src/agents/tools/cron-tool.ts | 46 +++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4a7005d..44a2e6021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Status: unreleased. ### Changes - Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. - Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. +- Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. ## 2026.1.24-3 diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index a1d218dd7..739b3ada3 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -133,8 +133,50 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { return { label: "Cron", name: "cron", - description: - "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use `jobId` as the canonical identifier; `id` is accepted for compatibility. Use `contextMessages` (0-10) to add previous messages as context to the job text.", + description: `Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. + +ACTIONS: +- status: Check cron scheduler status +- list: List jobs (use includeDisabled:true to include disabled) +- add: Create job (requires job object, see schema below) +- update: Modify job (requires jobId + patch object) +- remove: Delete job (requires jobId) +- run: Trigger job immediately (requires jobId) +- runs: Get job run history (requires jobId) +- wake: Send wake event (requires text, optional mode) + +JOB SCHEMA (for add action): +{ + "name": "string (optional)", + "schedule": { ... }, // Required: when to run + "payload": { ... }, // Required: what to execute + "sessionTarget": "main" | "isolated", // Required + "enabled": true | false // Optional, default true +} + +SCHEDULE TYPES (schedule.kind): +- "at": One-shot at absolute time + { "kind": "at", "atMs": } +- "every": Recurring interval + { "kind": "every", "everyMs": , "anchorMs": } +- "cron": Cron expression + { "kind": "cron", "expr": "", "tz": "" } + +PAYLOAD TYPES (payload.kind): +- "systemEvent": Injects text as system event into session + { "kind": "systemEvent", "text": "" } +- "agentTurn": Runs agent with message (isolated sessions only) + { "kind": "agentTurn", "message": "", "model": "", "thinking": "", "timeoutSeconds": , "deliver": , "channel": "", "to": "", "bestEffortDeliver": } + +CRITICAL CONSTRAINTS: +- sessionTarget="main" REQUIRES payload.kind="systemEvent" +- sessionTarget="isolated" REQUIRES payload.kind="agentTurn" + +WAKE MODES (for wake action): +- "next-heartbeat" (default): Wake on next heartbeat +- "now": Wake immediately + +Use jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.`, parameters: CronToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; From a21671ed5b3f034aa89940a53e375d19b199b1de Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:25:08 -0600 Subject: [PATCH 14/34] Skills: add missing dependency metadata (#1995) Co-authored-by: jackheuberger --- CHANGELOG.md | 1 + skills/discord/SKILL.md | 1 + skills/github/SKILL.md | 1 + skills/notion/SKILL.md | 2 +- skills/slack/SKILL.md | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a2e6021..425b21b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Status: unreleased. - Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. - Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. - Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. +- Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. ## 2026.1.24-3 diff --git a/skills/discord/SKILL.md b/skills/discord/SKILL.md index 0b64f14e1..5525a3bf5 100644 --- a/skills/discord/SKILL.md +++ b/skills/discord/SKILL.md @@ -1,6 +1,7 @@ --- name: discord description: Use when you need to control Discord from Clawdbot via the discord tool: send messages, react, post or upload stickers, upload emojis, run polls, manage threads/pins/search, create/edit/delete channels and categories, fetch permissions or member/role/channel info, or handle moderation actions in Discord DMs or channels. +metadata: {"clawdbot":{"emoji":"🎮","requires":{"config":["channels.discord"]}}} --- # Discord Actions diff --git a/skills/github/SKILL.md b/skills/github/SKILL.md index 03b2a0033..e7c89f7ba 100644 --- a/skills/github/SKILL.md +++ b/skills/github/SKILL.md @@ -1,6 +1,7 @@ --- name: github description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries." +metadata: {"clawdbot":{"emoji":"🐙","requires":{"bins":["gh"]},"install":[{"id":"brew","kind":"brew","formula":"gh","bins":["gh"],"label":"Install GitHub CLI (brew)"},{"id":"apt","kind":"apt","package":"gh","bins":["gh"],"label":"Install GitHub CLI (apt)"}]}} --- # GitHub Skill diff --git a/skills/notion/SKILL.md b/skills/notion/SKILL.md index 869871b3c..04921e250 100644 --- a/skills/notion/SKILL.md +++ b/skills/notion/SKILL.md @@ -2,7 +2,7 @@ name: notion description: Notion API for creating and managing pages, databases, and blocks. homepage: https://developers.notion.com -metadata: {"clawdbot":{"emoji":"📝"}} +metadata: {"clawdbot":{"emoji":"📝","requires":{"env":["NOTION_API_KEY"]},"primaryEnv":"NOTION_API_KEY"}} --- # notion diff --git a/skills/slack/SKILL.md b/skills/slack/SKILL.md index df04f858f..b72bab1f3 100644 --- a/skills/slack/SKILL.md +++ b/skills/slack/SKILL.md @@ -1,6 +1,7 @@ --- name: slack description: Use when you need to control Slack from Clawdbot via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs. +metadata: {"clawdbot":{"emoji":"💬","requires":{"config":["channels.slack"]}}} --- # Slack Actions From 136f0d4d1d5028516f4824314a6db5ebd06871af Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:28:53 -0600 Subject: [PATCH 15/34] Docs: add Render deployment guide (#1975) Co-authored-by: Anurag Goel --- CHANGELOG.md | 1 + docs/docs.json | 1 + docs/render.mdx | 158 ++++++++++++++++++++++++++++++++++++++++++++++++ render.yaml | 21 +++++++ 4 files changed, 181 insertions(+) create mode 100644 docs/render.mdx create mode 100644 render.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 425b21b1e..6abd9fc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Status: unreleased. - Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. - Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. - Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. +- Docs: add Render deployment guide. (#1975) Thanks @anurag. ## 2026.1.24-3 diff --git a/docs/docs.json b/docs/docs.json index 4af7943e0..983585bff 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -827,6 +827,7 @@ "install/nix", "install/docker", "railway", + "render", "install/bun" ] }, diff --git a/docs/render.mdx b/docs/render.mdx new file mode 100644 index 000000000..3fcdae07a --- /dev/null +++ b/docs/render.mdx @@ -0,0 +1,158 @@ +--- +title: Deploy on Render +--- + +Deploy Clawdbot on Render using Infrastructure as Code. The included `render.yaml` Blueprint defines your entire stack declaratively, service, disk, environment variables, so you can deploy with a single click and version your infrastructure alongside your code. + +## Prerequisites + +- A [Render account](https://render.com) (free tier available) +- An API key from your preferred [model provider](/providers) + +## Deploy with a Render Blueprint + +Deploy to Render + +Clicking this link will: + +1. Create a new Render service from the `render.yaml` Blueprint at the root of this repo. +2. Prompt you to set `SETUP_PASSWORD` +3. Build the Docker image and deploy + +Once deployed, your service URL follows the pattern `https://.onrender.com`. + +## Understanding the Blueprint + +Render Blueprints are YAML files that define your infrastructure. The `render.yaml` in this +repository configures everything needed to run Clawdbot: + +```yaml +services: + - type: web + name: clawdbot + runtime: docker + plan: starter + healthCheckPath: /health + envVars: + - key: PORT + value: "8080" + - key: SETUP_PASSWORD + sync: false # prompts during deploy + - key: CLAWDBOT_STATE_DIR + value: /data/.clawdbot + - key: CLAWDBOT_WORKSPACE_DIR + value: /data/workspace + - key: CLAWDBOT_GATEWAY_TOKEN + generateValue: true # auto-generates a secure token + disk: + name: clawdbot-data + mountPath: /data + sizeGB: 1 +``` + +Key Blueprint features used: + +| Feature | Purpose | +|---------|---------| +| `runtime: docker` | Builds from the repo's Dockerfile | +| `healthCheckPath` | Render monitors `/health` and restarts unhealthy instances | +| `sync: false` | Prompts for value during deploy (secrets) | +| `generateValue: true` | Auto-generates a cryptographically secure value | +| `disk` | Persistent storage that survives redeploys | + +## Choosing a plan + +| Plan | Spin-down | Disk | Best for | +|------|-----------|------|----------| +| Free | After 15 min idle | Not available | Testing, demos | +| Starter | Never | 1GB+ | Personal use, small teams | +| Standard+ | Never | 1GB+ | Production, multiple channels | + +The Blueprint defaults to `starter`. To use free tier, change `plan: free` in your fork's +`render.yaml` (but note: no persistent disk means config resets on each deploy). + +## After deployment + +### Complete the setup wizard + +1. Navigate to `https://.onrender.com/setup` +2. Enter your `SETUP_PASSWORD` +3. Select a model provider and paste your API key +4. Optionally configure messaging channels (Telegram, Discord, Slack) +5. Click **Run setup** + +### Access the Control UI + +The web dashboard is available at `https://.onrender.com/clawdbot`. + +## Render Dashboard features + +### Logs + +View real-time logs in **Dashboard → your service → Logs**. Filter by: +- Build logs (Docker image creation) +- Deploy logs (service startup) +- Runtime logs (application output) + +### Shell access + +For debugging, open a shell session via **Dashboard → your service → Shell**. The persistent disk is mounted at `/data`. + +### Environment variables + +Modify variables in **Dashboard → your service → Environment**. Changes trigger an automatic redeploy. + +### Auto-deploy + +If you use the original Clawdbot repository, Render will not auto-deploy your Clawdbot. To update it, run a manual Blueprint sync from the dashboard. + +## Custom domain + +1. Go to **Dashboard → your service → Settings → Custom Domains** +2. Add your domain +3. Configure DNS as instructed (CNAME to `*.onrender.com`) +4. Render provisions a TLS certificate automatically + +## Scaling + +Render supports horizontal and vertical scaling: + +- **Vertical**: Change the plan to get more CPU/RAM +- **Horizontal**: Increase instance count (Standard plan and above) + +For Clawdbot, vertical scaling is usually sufficient. Horizontal scaling requires sticky sessions or external state management. + +## Backups and migration + +Export your configuration and workspace at any time: + +``` +https://.onrender.com/setup/export +``` + +This downloads a portable backup you can restore on any Clawdbot host. + +## Troubleshooting + +### Service won't start + +Check the deploy logs in the Render Dashboard. Common issues: + +- Missing `SETUP_PASSWORD` — the Blueprint prompts for this, but verify it's set +- Port mismatch — ensure `PORT=8080` matches the Dockerfile's exposed port + +### Slow cold starts (free tier) + +Free tier services spin down after 15 minutes of inactivity. The first request after spin-down takes a few seconds while the container starts. Upgrade to Starter plan for always-on. + +### Data loss after redeploy + +This happens on free tier (no persistent disk). Upgrade to a paid plan, or +regularly export your config via `/setup/export`. + +### Health check failures + +Render expects a 200 response from `/health` within 30 seconds. If builds succeed but deploys fail, the service may be taking too long to start. Check: + +- Build logs for errors +- Whether the container runs locally with `docker build && docker run` diff --git a/render.yaml b/render.yaml new file mode 100644 index 000000000..01923a8f6 --- /dev/null +++ b/render.yaml @@ -0,0 +1,21 @@ +services: + - type: web + name: clawdbot + runtime: docker + plan: starter + healthCheckPath: /health + envVars: + - key: PORT + value: "8080" + - key: SETUP_PASSWORD + sync: false + - key: CLAWDBOT_STATE_DIR + value: /data/.clawdbot + - key: CLAWDBOT_WORKSPACE_DIR + value: /data/workspace + - key: CLAWDBOT_GATEWAY_TOKEN + generateValue: true + disk: + name: clawdbot-data + mountPath: /data + sizeGB: 1 From 6b6284c69cda6193bc0de5d178ed0e8e0ea251e2 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:37:20 -0600 Subject: [PATCH 16/34] CI: add PR labeler + label sync --- .github/labeler.yml | 150 ++++++++++++++++++++++++++++ .github/workflows/auto-response.yml | 59 +++++++++++ .github/workflows/labeler.yml | 17 ++++ scripts/sync-labels.ts | 91 +++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/auto-response.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 scripts/sync-labels.ts diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..0f3344acc --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,150 @@ +"channel: bluebubbles": + - "extensions/bluebubbles/**" + - "docs/channels/bluebubbles.md" +"channel: discord": + - "src/discord/**" + - "extensions/discord/**" + - "docs/channels/discord.md" +"channel: googlechat": + - "extensions/googlechat/**" + - "docs/channels/googlechat.md" +"channel: imessage": + - "src/imessage/**" + - "extensions/imessage/**" + - "docs/channels/imessage.md" +"channel: line": + - "extensions/line/**" +"channel: matrix": + - "extensions/matrix/**" + - "docs/channels/matrix.md" +"channel: mattermost": + - "extensions/mattermost/**" + - "docs/channels/mattermost.md" +"channel: msteams": + - "extensions/msteams/**" + - "docs/channels/msteams.md" +"channel: nextcloud-talk": + - "extensions/nextcloud-talk/**" + - "docs/channels/nextcloud-talk.md" +"channel: nostr": + - "extensions/nostr/**" + - "docs/channels/nostr.md" +"channel: signal": + - "src/signal/**" + - "extensions/signal/**" + - "docs/channels/signal.md" +"channel: slack": + - "src/slack/**" + - "extensions/slack/**" + - "docs/channels/slack.md" +"channel: telegram": + - "src/telegram/**" + - "extensions/telegram/**" + - "docs/channels/telegram.md" +"channel: tlon": + - "extensions/tlon/**" + - "docs/channels/tlon.md" +"channel: voice-call": + - "extensions/voice-call/**" +"channel: whatsapp-web": + - "src/web/**" + - "extensions/whatsapp/**" + - "docs/channels/whatsapp.md" +"channel: zalo": + - "extensions/zalo/**" + - "docs/channels/zalo.md" +"channel: zalouser": + - "extensions/zalouser/**" + - "docs/channels/zalouser.md" + +"app: android": + - "apps/android/**" + - "docs/platforms/android.md" +"app: ios": + - "apps/ios/**" + - "docs/platforms/ios.md" +"app: macos": + - "apps/macos/**" + - "docs/platforms/macos.md" + - "docs/platforms/mac/**" +"app: web-ui": + - "ui/**" + - "src/gateway/control-ui.ts" + - "src/gateway/control-ui-shared.ts" + - "src/infra/control-ui-assets.ts" + +"cli": + - "src/cli/**" + - "src/commands/**" + - "src/tui/**" + +"gateway": + - "src/gateway/**" + - "src/daemon/**" + - "docs/gateway/**" + +"docs": + - "docs/**" + - "docs.acp.md" + - "README.md" + - "README-header.png" + - "CHANGELOG.md" + - "CONTRIBUTING.md" + - "SECURITY.md" + +"extensions: bluebubbles": + - "extensions/bluebubbles/**" +"extensions: copilot-proxy": + - "extensions/copilot-proxy/**" +"extensions: diagnostics-otel": + - "extensions/diagnostics-otel/**" +"extensions: discord": + - "extensions/discord/**" +"extensions: google-antigravity-auth": + - "extensions/google-antigravity-auth/**" +"extensions: google-gemini-cli-auth": + - "extensions/google-gemini-cli-auth/**" +"extensions: googlechat": + - "extensions/googlechat/**" +"extensions: imessage": + - "extensions/imessage/**" +"extensions: line": + - "extensions/line/**" +"extensions: llm-task": + - "extensions/llm-task/**" +"extensions: lobster": + - "extensions/lobster/**" +"extensions: matrix": + - "extensions/matrix/**" +"extensions: mattermost": + - "extensions/mattermost/**" +"extensions: memory-core": + - "extensions/memory-core/**" +"extensions: memory-lancedb": + - "extensions/memory-lancedb/**" +"extensions: msteams": + - "extensions/msteams/**" +"extensions: nextcloud-talk": + - "extensions/nextcloud-talk/**" +"extensions: nostr": + - "extensions/nostr/**" +"extensions: open-prose": + - "extensions/open-prose/**" +"extensions: qwen-portal-auth": + - "extensions/qwen-portal-auth/**" +"extensions: signal": + - "extensions/signal/**" +"extensions: slack": + - "extensions/slack/**" +"extensions: telegram": + - "extensions/telegram/**" +"extensions: tlon": + - "extensions/tlon/**" +"extensions: voice-call": + - "extensions/voice-call/**" +"extensions: whatsapp": + - "extensions/whatsapp/**" +"extensions: zalo": + - "extensions/zalo/**" +"extensions: zalouser": + - "extensions/zalouser/**" diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml new file mode 100644 index 000000000..7f242a094 --- /dev/null +++ b/.github/workflows/auto-response.yml @@ -0,0 +1,59 @@ +name: Auto response + +on: + issues: + types: [labeled] + pull_request: + types: [labeled] + +permissions: + issues: write + pull-requests: write + +jobs: + auto-response: + runs-on: ubuntu-latest + steps: + - name: Handle labeled items + uses: actions/github-script@v7 + with: + script: | + const rules = [ + { + label: "skill-clawdhub", + close: true, + message: + "Thanks for the contribution! New skills should be published to Clawdhub for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.", + }, + ]; + + const labelName = context.payload.label?.name; + if (!labelName) { + return; + } + + const rule = rules.find((item) => item.label === labelName); + if (!rule) { + return; + } + + const issueNumber = context.payload.issue?.number ?? context.payload.pull_request?.number; + if (!issueNumber) { + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: rule.message, + }); + + if (rule.close) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: "closed", + }); + } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..6ec73a1a3 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +name: Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml diff --git a/scripts/sync-labels.ts b/scripts/sync-labels.ts new file mode 100644 index 000000000..0220e911a --- /dev/null +++ b/scripts/sync-labels.ts @@ -0,0 +1,91 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import yaml from "yaml"; + +type LabelConfig = Record; + +type RepoLabel = { + name: string; + color?: string; +}; + +const COLOR_BY_PREFIX = new Map([ + ["channel", "1d76db"], + ["app", "6f42c1"], + ["extensions", "0e8a16"], + ["docs", "0075ca"], + ["cli", "f9d0c4"], + ["gateway", "d4c5f9"], +]); + +const configPath = resolve(".github/labeler.yml"); +const config = yaml.parse(readFileSync(configPath, "utf8")) as LabelConfig; + +if (!config || typeof config !== "object") { + throw new Error("labeler.yml must be a mapping of label names to globs."); +} + +const labelNames = Object.keys(config).filter(Boolean); +const repo = resolveRepo(); +const existing = fetchExistingLabels(repo); + +const missing = labelNames.filter((label) => !existing.has(label)); +if (!missing.length) { + console.log("All labeler labels already exist."); + process.exit(0); +} + +for (const label of missing) { + const color = pickColor(label); + execFileSync( + "gh", + [ + "api", + "-X", + "POST", + `repos/${repo}/labels`, + "-f", + `name=${label}`, + "-f", + `color=${color}`, + ], + { stdio: "inherit" }, + ); + console.log(`Created label: ${label}`); +} + +function pickColor(label: string): string { + const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim(); + return COLOR_BY_PREFIX.get(prefix) ?? "ededed"; +} + +function resolveRepo(): string { + const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], { + encoding: "utf8", + }).trim(); + + if (!remote) { + throw new Error("Unable to determine repository from git remote."); + } + + if (remote.startsWith("git@github.com:")) { + return remote.replace("git@github.com:", "").replace(/\.git$/, ""); + } + + if (remote.startsWith("https://github.com/")) { + return remote.replace("https://github.com/", "").replace(/\.git$/, ""); + } + + throw new Error(`Unsupported GitHub remote: ${remote}`); +} + +function fetchExistingLabels(repo: string): Map { + const raw = execFileSync( + "gh", + ["api", `repos/${repo}/labels?per_page=100`, "--paginate"], + { encoding: "utf8" }, + ); + const labels = JSON.parse(raw) as RepoLabel[]; + return new Map(labels.map((label) => [label.name, label])); +} From b25fcaef0f14293886f020231e169be51bb3da45 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:38:44 -0600 Subject: [PATCH 17/34] CI: parse labeler without deps --- scripts/sync-labels.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/scripts/sync-labels.ts b/scripts/sync-labels.ts index 0220e911a..297644c1e 100644 --- a/scripts/sync-labels.ts +++ b/scripts/sync-labels.ts @@ -1,9 +1,6 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; -import yaml from "yaml"; - -type LabelConfig = Record; type RepoLabel = { name: string; @@ -20,13 +17,12 @@ const COLOR_BY_PREFIX = new Map([ ]); const configPath = resolve(".github/labeler.yml"); -const config = yaml.parse(readFileSync(configPath, "utf8")) as LabelConfig; +const labelNames = extractLabelNames(readFileSync(configPath, "utf8")); -if (!config || typeof config !== "object") { - throw new Error("labeler.yml must be a mapping of label names to globs."); +if (!labelNames.length) { + throw new Error("labeler.yml must declare at least one label."); } -const labelNames = Object.keys(config).filter(Boolean); const repo = resolveRepo(); const existing = fetchExistingLabels(repo); @@ -55,6 +51,26 @@ for (const label of missing) { console.log(`Created label: ${label}`); } +function extractLabelNames(contents: string): string[] { + const labels: string[] = []; + for (const line of contents.split("\n")) { + if (!line.trim() || line.trimStart().startsWith("#")) { + continue; + } + if (/^\s/.test(line)) { + continue; + } + const match = line.match(/^(["'])(.+)\1\s*:/) ?? line.match(/^([^:]+):/); + if (match) { + const name = (match[2] ?? match[1] ?? "").trim(); + if (name) { + labels.push(name); + } + } + } + return labels; +} + function pickColor(label: string): string { const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim(); return COLOR_BY_PREFIX.get(prefix) ?? "ededed"; From 28fe95ac5ef56c50bb5c7a8c47307fb83060ba71 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:39:44 -0600 Subject: [PATCH 18/34] Docs: note labeler updates --- .github/labeler.yml | 41 ----------------------------------------- AGENTS.md | 1 + 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 0f3344acc..0c3d863cf 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -86,65 +86,24 @@ "docs": - "docs/**" - "docs.acp.md" - - "README.md" - - "README-header.png" - - "CHANGELOG.md" - - "CONTRIBUTING.md" - - "SECURITY.md" -"extensions: bluebubbles": - - "extensions/bluebubbles/**" "extensions: copilot-proxy": - "extensions/copilot-proxy/**" "extensions: diagnostics-otel": - "extensions/diagnostics-otel/**" -"extensions: discord": - - "extensions/discord/**" "extensions: google-antigravity-auth": - "extensions/google-antigravity-auth/**" "extensions: google-gemini-cli-auth": - "extensions/google-gemini-cli-auth/**" -"extensions: googlechat": - - "extensions/googlechat/**" -"extensions: imessage": - - "extensions/imessage/**" -"extensions: line": - - "extensions/line/**" "extensions: llm-task": - "extensions/llm-task/**" "extensions: lobster": - "extensions/lobster/**" -"extensions: matrix": - - "extensions/matrix/**" -"extensions: mattermost": - - "extensions/mattermost/**" "extensions: memory-core": - "extensions/memory-core/**" "extensions: memory-lancedb": - "extensions/memory-lancedb/**" -"extensions: msteams": - - "extensions/msteams/**" -"extensions: nextcloud-talk": - - "extensions/nextcloud-talk/**" -"extensions: nostr": - - "extensions/nostr/**" "extensions: open-prose": - "extensions/open-prose/**" "extensions: qwen-portal-auth": - "extensions/qwen-portal-auth/**" -"extensions: signal": - - "extensions/signal/**" -"extensions: slack": - - "extensions/slack/**" -"extensions: telegram": - - "extensions/telegram/**" -"extensions: tlon": - - "extensions/tlon/**" -"extensions: voice-call": - - "extensions/voice-call/**" -"extensions: whatsapp": - - "extensions/whatsapp/**" -"extensions: zalo": - - "extensions/zalo/**" -"extensions: zalouser": - - "extensions/zalouser/**" diff --git a/AGENTS.md b/AGENTS.md index deed6d9bd..ac85a00d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ - Core channel docs: `docs/channels/` - Core channel code: `src/telegram`, `src/discord`, `src/slack`, `src/signal`, `src/imessage`, `src/web` (WhatsApp web), `src/channels`, `src/routing` - Extensions (channel plugins): `extensions/*` (e.g. `extensions/msteams`, `extensions/matrix`, `extensions/zalo`, `extensions/zalouser`, `extensions/voice-call`) +- When adding channels/extensions/apps/docs, review `.github/labeler.yml` for label coverage. ## Docs Linking (Mintlify) - Docs are hosted on Mintlify (docs.clawd.bot). From 9c8e8c5c2d531e58cfe7fe0714a4530fa10c8016 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 20:45:42 -0600 Subject: [PATCH 19/34] CI: increase Node heap size for macOS checks (#1890) Co-authored-by: Zach Knickerbocker --- .github/workflows/ci.yml | 2 ++ CHANGELOG.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcd8e457c..8cc86bd63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -342,6 +342,8 @@ jobs: pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true - name: Run ${{ matrix.task }} + env: + NODE_OPTIONS: --max-old-space-size=4096 run: ${{ matrix.command }} macos-app: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abd9fc53..93b171b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Status: unreleased. - Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. - Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. - Docs: add Render deployment guide. (#1975) Thanks @anurag. +- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. ## 2026.1.24-3 From 159f6bfddd6c9e596856fdac65b775c67ed5c364 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:02:18 -0600 Subject: [PATCH 20/34] macOS: bump Textual to 0.3.1 (#2033) Co-authored-by: Garric G. Nahapetian --- CHANGELOG.md | 1 + apps/macos/Package.resolved | 4 ++-- apps/shared/ClawdbotKit/Package.swift | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b171b38..19cea8844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Status: unreleased. - Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. - Docs: add Render deployment guide. (#1975) Thanks @anurag. - CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. +- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. ## 2026.1.24-3 diff --git a/apps/macos/Package.resolved b/apps/macos/Package.resolved index ffc524d1c..ef9609649 100644 --- a/apps/macos/Package.resolved +++ b/apps/macos/Package.resolved @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/textual", "state" : { - "revision" : "a03c1e103d88de4ea0dd8320ea1611ec0d4b29b3", - "version" : "0.2.0" + "revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38", + "version" : "0.3.1" } } ], diff --git a/apps/shared/ClawdbotKit/Package.swift b/apps/shared/ClawdbotKit/Package.swift index 076842fce..88dc28b5c 100644 --- a/apps/shared/ClawdbotKit/Package.swift +++ b/apps/shared/ClawdbotKit/Package.swift @@ -15,7 +15,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.0"), - .package(url: "https://github.com/gonzalezreal/textual", exact: "0.2.0"), + .package(url: "https://github.com/gonzalezreal/textual", exact: "0.3.1"), ], targets: [ .target( From 5d2ef89e0367b2301e2a5125e7e644277a803fa7 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:04:41 -0600 Subject: [PATCH 21/34] Browser: add URL fallback for relay tab matching (#1999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Paulo Furtado --- CHANGELOG.md | 1 + src/browser/pw-session.ts | 52 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cea8844..23d5d51b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Status: unreleased. - Docs: add Render deployment guide. (#1975) Thanks @anurag. - CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. - macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. +- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. ## 2026.1.24-3 diff --git a/src/browser/pw-session.ts b/src/browser/pw-session.ts index 0c7fa9f48..e1dbcf7a1 100644 --- a/src/browser/pw-session.ts +++ b/src/browser/pw-session.ts @@ -337,12 +337,56 @@ async function pageTargetId(page: Page): Promise { } } -async function findPageByTargetId(browser: Browser, targetId: string): Promise { +async function findPageByTargetId( + browser: Browser, + targetId: string, + cdpUrl?: string, +): Promise { const pages = await getAllPages(browser); + // First, try the standard CDP session approach for (const page of pages) { const tid = await pageTargetId(page).catch(() => null); if (tid && tid === targetId) return page; } + // If CDP sessions fail (e.g., extension relay blocks Target.attachToBrowserTarget), + // fall back to URL-based matching using the /json/list endpoint + if (cdpUrl) { + try { + const baseUrl = cdpUrl + .replace(/\/+$/, "") + .replace(/^ws:/, "http:") + .replace(/\/cdp$/, ""); + const response = await fetch(`${baseUrl}/json/list`); + if (response.ok) { + const targets = (await response.json()) as Array<{ + id: string; + url: string; + title?: string; + }>; + const target = targets.find((t) => t.id === targetId); + if (target) { + // Try to find a page with matching URL + const urlMatch = pages.filter((p) => p.url() === target.url); + if (urlMatch.length === 1) { + return urlMatch[0]; + } + // If multiple URL matches, use index-based matching as fallback + // This works when Playwright and the relay enumerate tabs in the same order + if (urlMatch.length > 1) { + const sameUrlTargets = targets.filter((t) => t.url === target.url); + if (sameUrlTargets.length === urlMatch.length) { + const idx = sameUrlTargets.findIndex((t) => t.id === targetId); + if (idx >= 0 && idx < urlMatch.length) { + return urlMatch[idx]; + } + } + } + } + } + } catch { + // Ignore fetch errors and fall through to return null + } + } return null; } @@ -355,7 +399,7 @@ export async function getPageForTargetId(opts: { if (!pages.length) throw new Error("No pages available in the connected browser."); const first = pages[0]; if (!opts.targetId) return first; - const found = await findPageByTargetId(browser, opts.targetId); + const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl); if (!found) { // Extension relays can block CDP attachment APIs (e.g. Target.attachToBrowserTarget), // which prevents us from resolving a page's targetId via newCDPSession(). If Playwright @@ -496,7 +540,7 @@ export async function closePageByTargetIdViaPlaywright(opts: { targetId: string; }): Promise { const { browser } = await connectBrowser(opts.cdpUrl); - const page = await findPageByTargetId(browser, opts.targetId); + const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl); if (!page) { throw new Error("tab not found"); } @@ -512,7 +556,7 @@ export async function focusPageByTargetIdViaPlaywright(opts: { targetId: string; }): Promise { const { browser } = await connectBrowser(opts.cdpUrl); - const page = await findPageByTargetId(browser, opts.targetId); + const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl); if (!page) { throw new Error("tab not found"); } From 6d60c325700e26ad0876be74ceb29d1b0e3a4648 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:07:51 -0600 Subject: [PATCH 22/34] Update: ignore dist/control-ui in dirty check (#1976) Co-authored-by: Glucksberg --- CHANGELOG.md | 1 + src/infra/update-check.ts | 7 ++++--- src/infra/update-runner.test.ts | 7 ++++--- src/infra/update-runner.ts | 19 +++++++++++++++++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d5d51b3..a1e2a9d08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status: unreleased. - CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. - macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. - Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. +- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. ## 2026.1.24-3 diff --git a/src/infra/update-check.ts b/src/infra/update-check.ts index 2e020ff8d..518da3c28 100644 --- a/src/infra/update-check.ts +++ b/src/infra/update-check.ts @@ -129,9 +129,10 @@ export async function checkGitUpdateStatus(params: { ).catch(() => null); const upstream = upstreamRes && upstreamRes.code === 0 ? upstreamRes.stdout.trim() : null; - const dirtyRes = await runCommandWithTimeout(["git", "-C", root, "status", "--porcelain"], { - timeoutMs, - }).catch(() => null); + const dirtyRes = await runCommandWithTimeout( + ["git", "-C", root, "status", "--porcelain", "--", ":!dist/control-ui/"], + { timeoutMs }, + ).catch(() => null); const dirty = dirtyRes && dirtyRes.code === 0 ? dirtyRes.stdout.trim().length > 0 : null; const fetchOk = params.fetch diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index e33159326..6bf450d83 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -44,7 +44,7 @@ describe("runGatewayUpdate", () => { [`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir }, [`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" }, [`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" }, - [`git -C ${tempDir} status --porcelain`]: { stdout: " M README.md" }, + [`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: " M README.md" }, }); const result = await runGatewayUpdate({ @@ -69,7 +69,7 @@ describe("runGatewayUpdate", () => { [`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir }, [`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" }, [`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" }, - [`git -C ${tempDir} status --porcelain`]: { stdout: "" }, + [`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: "" }, [`git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`]: { stdout: "origin/main", }, @@ -103,7 +103,7 @@ describe("runGatewayUpdate", () => { const { runner, calls } = createRunner({ [`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir }, [`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" }, - [`git -C ${tempDir} status --porcelain`]: { stdout: "" }, + [`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: "" }, [`git -C ${tempDir} fetch --all --prune --tags`]: { stdout: "" }, [`git -C ${tempDir} tag --list v* --sort=-v:refname`]: { stdout: `${stableTag}\n${betaTag}\n`, @@ -112,6 +112,7 @@ describe("runGatewayUpdate", () => { "pnpm install": { stdout: "" }, "pnpm build": { stdout: "" }, "pnpm ui:build": { stdout: "" }, + [`git -C ${tempDir} checkout -- dist/control-ui/`]: { stdout: "" }, "pnpm clawdbot doctor --non-interactive": { stdout: "" }, }); diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 0a5196fd7..c73c3a7e7 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -346,10 +346,14 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< const channel: UpdateChannel = opts.channel ?? "dev"; const branch = channel === "dev" ? await readBranchName(runCommand, gitRoot, timeoutMs) : null; const needsCheckoutMain = channel === "dev" && branch !== DEV_BRANCH; - gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 10 : 9) : 8; + gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 11 : 10) : 9; const statusCheck = await runStep( - step("clean check", ["git", "-C", gitRoot, "status", "--porcelain"], gitRoot), + step( + "clean check", + ["git", "-C", gitRoot, "status", "--porcelain", "--", ":!dist/control-ui/"], + gitRoot, + ), ); steps.push(statusCheck); const hasUncommittedChanges = @@ -654,6 +658,17 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< ); steps.push(uiBuildStep); + // Restore dist/control-ui/ to committed state to prevent dirty repo after update + // (ui:build regenerates assets with new hashes, which would block future updates) + const restoreUiStep = await runStep( + step( + "restore control-ui", + ["git", "-C", gitRoot, "checkout", "--", "dist/control-ui/"], + gitRoot, + ), + ); + steps.push(restoreUiStep); + const doctorStep = await runStep( step( "clawdbot doctor", From a989fe8af92e5630f6b0f51e4156a0a21a47c346 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:08:12 -0600 Subject: [PATCH 23/34] CI: update labeler v5 config --- .github/labeler.yml | 205 +++++++++++++++++++++++++++++--------------- 1 file changed, 134 insertions(+), 71 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 0c3d863cf..5b34c41e0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,109 +1,172 @@ "channel: bluebubbles": - - "extensions/bluebubbles/**" - - "docs/channels/bluebubbles.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/bluebubbles/**" + - "docs/channels/bluebubbles.md" "channel: discord": - - "src/discord/**" - - "extensions/discord/**" - - "docs/channels/discord.md" + - changed-files: + - any-glob-to-any-file: + - "src/discord/**" + - "extensions/discord/**" + - "docs/channels/discord.md" "channel: googlechat": - - "extensions/googlechat/**" - - "docs/channels/googlechat.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/googlechat/**" + - "docs/channels/googlechat.md" "channel: imessage": - - "src/imessage/**" - - "extensions/imessage/**" - - "docs/channels/imessage.md" + - changed-files: + - any-glob-to-any-file: + - "src/imessage/**" + - "extensions/imessage/**" + - "docs/channels/imessage.md" "channel: line": - - "extensions/line/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/line/**" "channel: matrix": - - "extensions/matrix/**" - - "docs/channels/matrix.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/matrix/**" + - "docs/channels/matrix.md" "channel: mattermost": - - "extensions/mattermost/**" - - "docs/channels/mattermost.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/mattermost/**" + - "docs/channels/mattermost.md" "channel: msteams": - - "extensions/msteams/**" - - "docs/channels/msteams.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/msteams/**" + - "docs/channels/msteams.md" "channel: nextcloud-talk": - - "extensions/nextcloud-talk/**" - - "docs/channels/nextcloud-talk.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/nextcloud-talk/**" + - "docs/channels/nextcloud-talk.md" "channel: nostr": - - "extensions/nostr/**" - - "docs/channels/nostr.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/nostr/**" + - "docs/channels/nostr.md" "channel: signal": - - "src/signal/**" - - "extensions/signal/**" - - "docs/channels/signal.md" + - changed-files: + - any-glob-to-any-file: + - "src/signal/**" + - "extensions/signal/**" + - "docs/channels/signal.md" "channel: slack": - - "src/slack/**" - - "extensions/slack/**" - - "docs/channels/slack.md" + - changed-files: + - any-glob-to-any-file: + - "src/slack/**" + - "extensions/slack/**" + - "docs/channels/slack.md" "channel: telegram": - - "src/telegram/**" - - "extensions/telegram/**" - - "docs/channels/telegram.md" + - changed-files: + - any-glob-to-any-file: + - "src/telegram/**" + - "extensions/telegram/**" + - "docs/channels/telegram.md" "channel: tlon": - - "extensions/tlon/**" - - "docs/channels/tlon.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/tlon/**" + - "docs/channels/tlon.md" "channel: voice-call": - - "extensions/voice-call/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/voice-call/**" "channel: whatsapp-web": - - "src/web/**" - - "extensions/whatsapp/**" - - "docs/channels/whatsapp.md" + - changed-files: + - any-glob-to-any-file: + - "src/web/**" + - "extensions/whatsapp/**" + - "docs/channels/whatsapp.md" "channel: zalo": - - "extensions/zalo/**" - - "docs/channels/zalo.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/zalo/**" + - "docs/channels/zalo.md" "channel: zalouser": - - "extensions/zalouser/**" - - "docs/channels/zalouser.md" + - changed-files: + - any-glob-to-any-file: + - "extensions/zalouser/**" + - "docs/channels/zalouser.md" "app: android": - - "apps/android/**" - - "docs/platforms/android.md" + - changed-files: + - any-glob-to-any-file: + - "apps/android/**" + - "docs/platforms/android.md" "app: ios": - - "apps/ios/**" - - "docs/platforms/ios.md" + - changed-files: + - any-glob-to-any-file: + - "apps/ios/**" + - "docs/platforms/ios.md" "app: macos": - - "apps/macos/**" - - "docs/platforms/macos.md" - - "docs/platforms/mac/**" + - changed-files: + - any-glob-to-any-file: + - "apps/macos/**" + - "docs/platforms/macos.md" + - "docs/platforms/mac/**" "app: web-ui": - - "ui/**" - - "src/gateway/control-ui.ts" - - "src/gateway/control-ui-shared.ts" - - "src/infra/control-ui-assets.ts" - -"cli": - - "src/cli/**" - - "src/commands/**" - - "src/tui/**" + - changed-files: + - any-glob-to-any-file: + - "ui/**" + - "src/gateway/control-ui.ts" + - "src/gateway/control-ui-shared.ts" + - "src/infra/control-ui-assets.ts" "gateway": - - "src/gateway/**" - - "src/daemon/**" - - "docs/gateway/**" + - changed-files: + - any-glob-to-any-file: + - "src/gateway/**" + - "src/daemon/**" + - "docs/gateway/**" "docs": - - "docs/**" - - "docs.acp.md" + - changed-files: + - any-glob-to-any-file: + - "docs/**" + - "docs.acp.md" "extensions: copilot-proxy": - - "extensions/copilot-proxy/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/copilot-proxy/**" "extensions: diagnostics-otel": - - "extensions/diagnostics-otel/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/diagnostics-otel/**" "extensions: google-antigravity-auth": - - "extensions/google-antigravity-auth/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/google-antigravity-auth/**" "extensions: google-gemini-cli-auth": - - "extensions/google-gemini-cli-auth/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/google-gemini-cli-auth/**" "extensions: llm-task": - - "extensions/llm-task/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/llm-task/**" "extensions: lobster": - - "extensions/lobster/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/lobster/**" "extensions: memory-core": - - "extensions/memory-core/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/memory-core/**" "extensions: memory-lancedb": - - "extensions/memory-lancedb/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/memory-lancedb/**" "extensions: open-prose": - - "extensions/open-prose/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/open-prose/**" "extensions: qwen-portal-auth": - - "extensions/qwen-portal-auth/**" + - changed-files: + - any-glob-to-any-file: + - "extensions/qwen-portal-auth/**" From 47101da4643ab499831a8b0377422d13f46093da Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:09:31 -0600 Subject: [PATCH 24/34] Telegram: honor caption param for media sends (#1888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marc Güell Segarra --- CHANGELOG.md | 1 + src/channels/plugins/actions/telegram.ts | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e2a9d08..7bb0a459d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Status: unreleased. - macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. - Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. - Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. +- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. ## 2026.1.24-3 diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 18a11c797..fe4e41307 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -13,11 +13,9 @@ const providerId = "telegram"; function readTelegramSendParams(params: Record) { const to = readStringParam(params, "to", { required: true }); const mediaUrl = readStringParam(params, "media", { trim: false }); - const content = - readStringParam(params, "message", { - required: !mediaUrl, - allowEmpty: true, - }) ?? ""; + const message = readStringParam(params, "message", { required: !mediaUrl, allowEmpty: true }); + const caption = readStringParam(params, "caption", { allowEmpty: true }); + const content = message || caption || ""; const replyTo = readStringParam(params, "replyTo"); const threadId = readStringParam(params, "threadId"); const buttons = params.buttons; From 84f8f8b10e540d2c89c1c475bdec3c3c94c6d592 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:11:50 -0600 Subject: [PATCH 25/34] Telegram: skip block replies when streaming off (#1885) Co-authored-by: Ivan Casco --- CHANGELOG.md | 1 + src/auto-reply/reply/agent-runner-execution.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb0a459d..af7ae9ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Status: unreleased. - Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. - Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. - Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. +- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. ## 2026.1.24-3 diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 47c45b09d..939fa92f0 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -369,12 +369,13 @@ export async function runAgentTurnWithFallback(params: { // Use pipeline if available (block streaming enabled), otherwise send directly if (params.blockStreamingEnabled && params.blockReplyPipeline) { params.blockReplyPipeline.enqueue(blockPayload); - } else { - // Send directly when flushing before tool execution (no streaming). + } else if (params.blockStreamingEnabled) { + // Send directly when flushing before tool execution (no pipeline but streaming enabled). // Track sent key to avoid duplicate in final payloads. directlySentBlockKeys.add(createBlockReplyPayloadKey(blockPayload)); await params.opts?.onBlockReply?.(blockPayload); } + // When streaming is disabled entirely, blocks are accumulated in final text instead. } : undefined, onBlockReplyFlush: From 9ecbb0ae81db993dc05962abef9118b53eb3d599 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:13:36 -0600 Subject: [PATCH 26/34] Auth: print copyable Google auth URL (#1787) Co-authored-by: Robby --- CHANGELOG.md | 1 + extensions/google-antigravity-auth/index.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af7ae9ddc..8d5412dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Status: unreleased. - Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. - Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. - Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. +- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. ## 2026.1.24-3 diff --git a/extensions/google-antigravity-auth/index.ts b/extensions/google-antigravity-auth/index.ts index d6902bffe..f349ada6a 100644 --- a/extensions/google-antigravity-auth/index.ts +++ b/extensions/google-antigravity-auth/index.ts @@ -281,6 +281,7 @@ async function loginAntigravity(params: { openUrl: (url: string) => Promise; prompt: (message: string) => Promise; note: (message: string, title?: string) => Promise; + log: (message: string) => void; progress: { update: (msg: string) => void; stop: (msg?: string) => void }; }): Promise<{ access: string; @@ -314,6 +315,11 @@ async function loginAntigravity(params: { ].join("\n"), "Google Antigravity OAuth", ); + // Output raw URL below the box for easy copying (fixes #1772) + params.log(""); + params.log("Copy this URL:"); + params.log(authUrl); + params.log(""); } if (!needsManual) { @@ -382,6 +388,7 @@ const antigravityPlugin = { openUrl: ctx.openUrl, prompt: async (message) => String(await ctx.prompter.text({ message })), note: ctx.prompter.note, + log: (message) => ctx.runtime.log(message), progress: spin, }); From 73507e8654abf751cce99696e6d91c4ac31ec917 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:15:20 -0600 Subject: [PATCH 27/34] Routing: precompile session key regexes (#1697) Co-authored-by: Ray Tien --- CHANGELOG.md | 1 + src/routing/session-key.ts | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5412dcd..e39c291d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Status: unreleased. - Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. - Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. +- Routing: precompile session key regexes. (#1697) Thanks @Ray0907. ## 2026.1.24-3 diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 028e657cb..7f9f209ed 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -11,6 +11,12 @@ export const DEFAULT_AGENT_ID = "main"; export const DEFAULT_MAIN_KEY = "main"; export const DEFAULT_ACCOUNT_ID = "default"; +// Pre-compiled regex +const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; +const INVALID_CHARS_RE = /[^a-z0-9_-]+/g; +const LEADING_DASH_RE = /^-+/; +const TRAILING_DASH_RE = /-+$/; + function normalizeToken(value: string | undefined | null): string { return (value ?? "").trim().toLowerCase(); } @@ -52,14 +58,14 @@ export function normalizeAgentId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); if (!trimmed) return DEFAULT_AGENT_ID; // Keep it path-safe + shell-friendly. - if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase(); + if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); // Best-effort fallback: collapse invalid characters to "-" return ( trimmed .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") + .replace(INVALID_CHARS_RE, "-") + .replace(LEADING_DASH_RE, "") + .replace(TRAILING_DASH_RE, "") .slice(0, 64) || DEFAULT_AGENT_ID ); } @@ -67,13 +73,13 @@ export function normalizeAgentId(value: string | undefined | null): string { export function sanitizeAgentId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); if (!trimmed) return DEFAULT_AGENT_ID; - if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase(); + if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); return ( trimmed .toLowerCase() - .replace(/[^a-z0-9_-]+/gi, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") + .replace(INVALID_CHARS_RE, "-") + .replace(LEADING_DASH_RE, "") + .replace(TRAILING_DASH_RE, "") .slice(0, 64) || DEFAULT_AGENT_ID ); } @@ -81,13 +87,13 @@ export function sanitizeAgentId(value: string | undefined | null): string { export function normalizeAccountId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); if (!trimmed) return DEFAULT_ACCOUNT_ID; - if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase(); + if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase(); return ( trimmed .toLowerCase() - .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") + .replace(INVALID_CHARS_RE, "-") + .replace(LEADING_DASH_RE, "") + .replace(TRAILING_DASH_RE, "") .slice(0, 64) || DEFAULT_ACCOUNT_ID ); } From 1f06f8031e7e16d93d6faee65e999a56179ce19b Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:15:34 -0600 Subject: [PATCH 28/34] CI: use app token for labeler --- .github/workflows/labeler.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 6ec73a1a3..8d078774b 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,6 +12,12 @@ jobs: label: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: "2729701" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - uses: actions/labeler@v5 with: configuration-path: .github/labeler.yml + repo-token: ${{ steps.app-token.outputs.token }} From 7187c3d06765c9d3a7b1de40430fe1567b174131 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:17:42 -0600 Subject: [PATCH 29/34] TUI: guard against overflow width crashes (#1686) Co-authored-by: Mohammad Jafari --- CHANGELOG.md | 1 + src/tui/components/filterable-select-list.ts | 2 +- src/tui/components/searchable-select-list.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e39c291d2..480767383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Status: unreleased. - Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. +- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. ## 2026.1.24-3 diff --git a/src/tui/components/filterable-select-list.ts b/src/tui/components/filterable-select-list.ts index 67361bcf1..a7b197bf5 100644 --- a/src/tui/components/filterable-select-list.ts +++ b/src/tui/components/filterable-select-list.ts @@ -69,7 +69,7 @@ export class FilterableSelectList implements Component { lines.push(filterLabel + inputText); // Separator - lines.push(chalk.dim("─".repeat(width))); + lines.push(chalk.dim("─".repeat(Math.max(0, width)))); // Select list const listLines = this.selectList.render(width); diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index f8e07e790..54fc34918 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -214,7 +214,8 @@ export class SearchableSelectList implements Component { const maxValueWidth = Math.min(30, width - prefixWidth - 4); const truncatedValue = truncateToWidth(displayValue, maxValueWidth, ""); const valueText = this.highlightMatch(truncatedValue, query); - const spacing = " ".repeat(Math.max(1, 32 - visibleWidth(valueText))); + const spacingWidth = Math.max(1, 32 - visibleWidth(valueText)); + const spacing = " ".repeat(spacingWidth); const descriptionStart = prefixWidth + visibleWidth(valueText) + spacing.length; const remainingWidth = width - descriptionStart - 2; if (remainingWidth > 10) { From 7f6422c8977ce782f15809fda26ae67a1d4c7aa9 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:20:39 -0600 Subject: [PATCH 30/34] Telegram: preserve topic IDs in restart notifications (#1807) Co-authored-by: hsrvc --- CHANGELOG.md | 1 + src/agents/tools/sessions-send-helpers.ts | 24 +++++++++++++++++++++-- src/gateway/server-restart-sentinel.ts | 16 ++++++++++----- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480767383..dc46291fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Status: unreleased. - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. +- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. ## 2026.1.24-3 diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 5e758d426..c9940de0f 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -14,6 +14,7 @@ export type AnnounceTarget = { channel: string; to: string; accountId?: string; + threadId?: string; // Forum topic/thread ID }; export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null { @@ -22,7 +23,22 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget if (parts.length < 3) return null; const [channelRaw, kind, ...rest] = parts; if (kind !== "group" && kind !== "channel") return null; - const id = rest.join(":").trim(); + + // Extract topic/thread ID from rest (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + let threadId: string | undefined; + const restJoined = rest.join(":"); + const topicMatch = restJoined.match(/:topic:(\d+)$/); + const threadMatch = restJoined.match(/:thread:(\d+)$/); + const match = topicMatch || threadMatch; + + if (match) { + threadId = match[1]; // Keep as string to match AgentCommandOpts.threadId + } + + // Remove :topic:N or :thread:N suffix from ID for target + const id = match ? restJoined.replace(/:(topic|thread):\d+$/, "") : restJoined.trim(); + if (!id) return null; if (!channelRaw) return null; const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw); @@ -37,7 +53,11 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget const normalized = normalizedChannel ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget) : undefined; - return { channel, to: normalized ?? kindTarget }; + return { + channel, + to: normalized ?? kindTarget, + threadId, + }; } export function buildAgentToAgentMessageContext(params: { diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index fa33b7c21..28719290e 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -28,11 +28,16 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { return; } - const threadMarker = ":thread:"; - const threadIndex = sessionKey.lastIndexOf(threadMarker); - const baseSessionKey = threadIndex === -1 ? sessionKey : sessionKey.slice(0, threadIndex); + // Extract topic/thread ID from sessionKey (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + const topicIndex = sessionKey.lastIndexOf(":topic:"); + const threadIndex = sessionKey.lastIndexOf(":thread:"); + const markerIndex = Math.max(topicIndex, threadIndex); + const marker = topicIndex > threadIndex ? ":topic:" : ":thread:"; + + const baseSessionKey = markerIndex === -1 ? sessionKey : sessionKey.slice(0, markerIndex); const threadIdRaw = - threadIndex === -1 ? undefined : sessionKey.slice(threadIndex + threadMarker.length); + markerIndex === -1 ? undefined : sessionKey.slice(markerIndex + marker.length); const sessionThreadId = threadIdRaw?.trim() || undefined; const { cfg, entry } = loadSessionEntry(sessionKey); @@ -42,7 +47,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { // Handles race condition where store wasn't flushed before restart const sentinelContext = payload.deliveryContext; let sessionDeliveryContext = deliveryContextFromSession(entry); - if (!sessionDeliveryContext && threadIndex !== -1 && baseSessionKey) { + if (!sessionDeliveryContext && markerIndex !== -1 && baseSessionKey) { const { entry: baseEntry } = loadSessionEntry(baseSessionKey); sessionDeliveryContext = deliveryContextFromSession(baseEntry); } @@ -74,6 +79,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { const threadId = payload.threadId ?? + parsedTarget?.threadId ?? // From resolveAnnounceTargetFromKey (extracts :topic:N) sessionThreadId ?? (origin?.threadId != null ? String(origin.threadId) : undefined); From 1b598ad70923e6f6c3f6f7bc12ffc75f06e07004 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:22:25 -0600 Subject: [PATCH 31/34] Config: apply config.env before substitution (#1813) Co-authored-by: SPANISH FLU --- CHANGELOG.md | 1 + src/config/io.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc46291fb..6aacd64aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Status: unreleased. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. +- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. ## 2026.1.24-3 diff --git a/src/config/io.ts b/src/config/io.ts index da3a7fb23..9078ef2a2 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -211,6 +211,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { parseJson: (raw) => deps.json5.parse(raw), }); + // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars + if (resolved && typeof resolved === "object" && "env" in resolved) { + applyConfigEnv(resolved as ClawdbotConfig, deps.env); + } + // Substitute ${VAR} env var references const substituted = resolveConfigEnvVars(resolved, deps.env); @@ -365,6 +370,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { }; } + // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars + if (resolved && typeof resolved === "object" && "env" in resolved) { + applyConfigEnv(resolved as ClawdbotConfig, deps.env); + } + // Substitute ${VAR} env var references let substituted: unknown; try { From 678ad9e3aed137e7ab65736185aacdc468f8e707 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:23:18 -0600 Subject: [PATCH 32/34] CI: expand web-ui label globs --- .github/labeler.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 5b34c41e0..5d2837a6c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -115,6 +115,8 @@ - "ui/**" - "src/gateway/control-ui.ts" - "src/gateway/control-ui-shared.ts" + - "src/gateway/protocol/**" + - "src/gateway/server-methods/chat.ts" - "src/infra/control-ui-assets.ts" "gateway": From 7e4e24445e21d0727895a2667ef6eac515e6904c Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:28:46 -0600 Subject: [PATCH 33/34] Slack: clear ack reaction after streaming replies (#2044) Co-authored-by: Shaurya Pratap Singh --- CHANGELOG.md | 1 + src/slack/monitor/message-handler/dispatch.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aacd64aa..2cd23f0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Status: unreleased. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. +- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. ## 2026.1.24-3 diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index d31885cfa..38b69f049 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -141,7 +141,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); markDispatchIdle(); - if (!queuedFinal) { + const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0; + + if (!anyReplyDelivered) { if (prepared.isRoomish) { clearHistoryEntriesIfEnabled({ historyMap: ctx.channelHistories, From 8b91ceb7c96f5eb15e3cda39d6fa6a769dddcfad Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:46:15 -0600 Subject: [PATCH 34/34] macOS: preserve custom SSH usernames (#2046) Co-authored-by: Alexis Gallagher --- CHANGELOG.md | 1 + apps/macos/Sources/Clawdbot/AppState.swift | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd23f0f1..5e3ab78da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Status: unreleased. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. +- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. ## 2026.1.24-3 diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index eeaf034d0..6ccb83369 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -413,10 +413,17 @@ final class AppState { } private func updateRemoteTarget(host: String) { - let parsed = CommandResolver.parseSSHTarget(self.remoteTarget) - let user = parsed?.user ?? NSUserName() - let port = parsed?.port ?? 22 - let assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" + let trimmed = self.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines) + guard let parsed = CommandResolver.parseSSHTarget(trimmed) else { return } + let trimmedUser = parsed.user?.trimmingCharacters(in: .whitespacesAndNewlines) + let user = (trimmedUser?.isEmpty ?? true) ? nil : trimmedUser + let port = parsed.port + let assembled: String + if let user { + assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" + } else { + assembled = port == 22 ? host : "\(host):\(port)" + } if assembled != self.remoteTarget { self.remoteTarget = assembled }