iMessage: promote BlueBubbles and refresh docs/skills (#8415)

* feat: Make BlueBubbles the primary iMessage integration

- Remove old imsg skill (skills/imsg/SKILL.md)
- Create new BlueBubbles skill (skills/bluebubbles/SKILL.md) with message tool examples
- Add keep-alive script documentation for VM/headless setups to docs/channels/bluebubbles.md
  - AppleScript that pokes Messages.app every 5 minutes
  - LaunchAgent configuration for automatic execution
  - Prevents Messages.app from going idle in VM environments
- Update all documentation to prioritize BlueBubbles over legacy imsg:
  - Mark imsg channel as legacy throughout docs
  - Update README.md channel lists
  - Update wizard, hubs, pairing, and index docs
  - Update FAQ to recommend BlueBubbles for iMessage
  - Update RPC docs to note imsg as legacy pattern
  - Update Chinese documentation (zh-CN)
- Replace imsg examples with generic macOS skill examples where appropriate

BlueBubbles is now the recommended first-class iMessage integration,
with the legacy imsg integration marked for potential future removal.

* refactor: Update import paths and improve code formatting

- Adjusted import paths in session-status-tool.ts, whatsapp-heartbeat.ts, and heartbeat-runner.ts for consistency.
- Reformatted code for better readability by aligning and grouping related imports and function parameters.
- Enhanced error messages and conditional checks for clarity in heartbeat-runner.ts.

* skills: restore imsg skill and align bluebubbles skill

* docs: update FAQ for clarity and formatting

- Adjusted the formatting of the FAQ section to ensure consistent bullet point alignment.
- No content changes were made, only formatting improvements for better readability.

* style: oxfmt touched files

* fix: preserve BlueBubbles developer reference (#8415) (thanks @tyler6204)
This commit is contained in:
Tyler Yust
2026-02-03 18:06:54 -08:00
committed by GitHub
parent 9c5941ba46
commit 9c4eab69cc
19 changed files with 534 additions and 269 deletions

View File

@@ -1,22 +1,6 @@
import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import type { AnyAgentTool } from "./common.js";
import { resolveAgentDir } from "../../agents/agent-scope.js";
import {
ensureAuthProfileStore,
resolveAuthProfileDisplayLabel,
resolveAuthProfileOrder,
} from "../../agents/auth-profiles.js";
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
import { loadModelCatalog } from "../../agents/model-catalog.js";
import {
buildAllowedModelSet,
buildModelAliasIndex,
modelKey,
normalizeProviderId,
resolveDefaultModelForAgent,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
import { buildStatusMessage } from "../../auto-reply/status.js";
@@ -39,7 +23,23 @@ import {
resolveAgentIdFromSessionKey,
} from "../../routing/session-key.js";
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
import { resolveAgentDir } from "../agent-scope.js";
import {
ensureAuthProfileStore,
resolveAuthProfileDisplayLabel,
resolveAuthProfileOrder,
} from "../auth-profiles.js";
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
import { getCustomProviderApiKey, resolveEnvApiKey } from "../model-auth.js";
import { loadModelCatalog } from "../model-catalog.js";
import {
buildAllowedModelSet,
buildModelAliasIndex,
modelKey,
normalizeProviderId,
resolveDefaultModelForAgent,
resolveModelRefFromString,
} from "../model-selection.js";
import { readStringParam } from "./common.js";
import {
shouldResolveSessionIdInput,

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeChatChannelId } from "../../channels/registry.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { normalizeE164 } from "../../utils.js";
import { normalizeChatChannelId } from "../registry.js";
type HeartbeatRecipientsResult = { recipients: string[]; source: string };
type HeartbeatRecipientsOpts = { to?: string; all?: boolean };

View File

@@ -34,13 +34,12 @@ import {
saveSessionStore,
updateSessionStore,
} from "../config/sessions.js";
import { formatErrorMessage } from "../infra/errors.js";
import { peekSystemEvents } from "../infra/system-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { getQueueSize } from "../process/command-queue.js";
import { CommandLane } from "../process/lanes.js";
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { formatErrorMessage } from "./errors.js";
import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
import {
@@ -54,6 +53,7 @@ import {
resolveHeartbeatDeliveryTarget,
resolveHeartbeatSenderContext,
} from "./outbound/targets.js";
import { peekSystemEvents } from "./system-events.js";
type HeartbeatDeps = OutboundSendDeps &
ChannelHeartbeatDeps & {
@@ -349,7 +349,9 @@ function resolveHeartbeatSession(
const mainSessionKey =
scope === "global" ? "global" : resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
const storeAgentId = scope === "global" ? resolveDefaultAgentId(cfg) : resolvedAgentId;
const storePath = resolveStorePath(sessionCfg?.store, { agentId: storeAgentId });
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: storeAgentId,
});
const store = loadSessionStore(storePath);
const mainEntry = store[mainSessionKey];
@@ -380,7 +382,12 @@ function resolveHeartbeatSession(
if (canonical !== "global") {
const sessionAgentId = resolveAgentIdFromSessionKey(canonical);
if (sessionAgentId === normalizeAgentId(resolvedAgentId)) {
return { sessionKey: canonical, storePath, store, entry: store[canonical] };
return {
sessionKey: canonical,
storePath,
store,
entry: store[canonical],
};
}
}
@@ -709,7 +716,11 @@ export async function runHeartbeatOnce(opts: {
}
if (!visibility.showAlerts) {
await restoreHeartbeatUpdatedAt({ storePath, sessionKey, updatedAt: previousUpdatedAt });
await restoreHeartbeatUpdatedAt({
storePath,
sessionKey,
updatedAt: previousUpdatedAt,
});
emitHeartbeatEvent({
status: "skipped",
reason: "alerts-disabled",
@@ -908,10 +919,16 @@ export function startHeartbeatRunner(opts: {
const run: HeartbeatWakeHandler = async (params) => {
if (!heartbeatsEnabled) {
return { status: "skipped", reason: "disabled" } satisfies HeartbeatRunResult;
return {
status: "skipped",
reason: "disabled",
} satisfies HeartbeatRunResult;
}
if (state.agents.size === 0) {
return { status: "skipped", reason: "disabled" } satisfies HeartbeatRunResult;
return {
status: "skipped",
reason: "disabled",
} satisfies HeartbeatRunResult;
}
const reason = params?.reason;