@clawling/clawchat-plugin-openclaw 2026.6.24-1 → 2026.6.30-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,20 +90,26 @@ src/
90
90
  api-types.ts /v1/* request/response type definitions
91
91
  inbound.ts envelope → agent turn
92
92
  outbound.ts agent reply → envelope
93
- streaming.ts progressive reply emitter
94
- buffered-stream.ts chunk coalescing + flush policy
95
93
  message-mapper.ts fragment ↔ text/media mapping
96
94
  group-message-coalescer.ts
97
95
  per-group idle/max batching for non-mention turns
96
+ group-settings.ts per-group agent settings cache (GET /v1/agents/me/
97
+ group-settings)
98
98
  reply-dispatcher.ts static vs stream routing
99
+ no-reply.ts clawchat:no-reply / :silent suppression token guard
99
100
  login.runtime.ts invite-code exchange flow
101
+ refresh-manager.ts ClawChat token refresh + auto-logout orchestration
102
+ skill-update.ts conversational, adapter-driven skill hot-update
100
103
  media-runtime.ts media download/upload pipeline
104
+ liveware-cli.ts liveware asset download helper (custom CDN User-Agent)
101
105
  tools.ts clawchat_* agent tools (account / media / memory /
102
106
  metadata / moments / mention)
103
107
  tools-schema.ts JSON schemas for the clawchat_* tools
104
108
  protocol.ts inbound payload type guards
105
109
  protocol-types.ts v2 envelope, fragment, transport shapes
106
110
  config.ts defaults + typebox schema
111
+ config-compat.ts migration for the plugin rename (openclaw-clawchat →
112
+ clawchat-plugin-openclaw id/channel key)
107
113
  storage.ts plugin-owned SQLite (activations, connections,
108
114
  clawchat_messages, tool_calls)
109
115
  clawchat-memory.ts file-backed memory tools (see docs/clawchat-memory.md)
@@ -114,6 +120,8 @@ src/
114
120
  plugin-prompts.ts loads prompts/platform.md and default-* prompts
115
121
  ws-alignment.ts handshake capabilities + hello-ok diagnostics
116
122
  ws-log.ts clawchat.ws single-line log formatting
123
+ llm-context-debug.ts opt-in LLM input/output snapshot capture for debugging
124
+ plugin-report.ts best-effort read of this plugin's package version
117
125
  commands.ts /clawchat-activate runtime slash command
118
126
  terminal-send.ts terminal-send suppression for clawchat_mention_message
119
127
  mock-transport.ts in-memory Transport double for tests
@@ -178,4 +186,4 @@ plugin tests or publish the package.
178
186
 
179
187
  ## License
180
188
 
181
- See the repository root.
189
+ MIT — see [LICENSE](./LICENSE).
@@ -418,6 +418,9 @@ export function createOpenclawClawlingApiClient(opts) {
418
418
  if (userId?.trim()) {
419
419
  body.user_id = userId.trim();
420
420
  }
421
+ if (opts.pluginVersion?.trim()) {
422
+ body.plugin_version = opts.pluginVersion.trim();
423
+ }
421
424
  return await call("POST", "/v1/agents/connect", {
422
425
  // `X-Device-Id` is added globally via `authHeaders` on every request.
423
426
  headers: { "content-type": "application/json" },
@@ -149,6 +149,27 @@ export async function dispatchOpenclawClawlingInbound(params) {
149
149
  chatType,
150
150
  userId: account.userId,
151
151
  });
152
+ // Pending-consent gate for conversational skill hot-update. Only owner direct
153
+ // messages can answer a pending skill-update ask; the gate parses consent
154
+ // conservatively and, when it consumes the reply (affirm/deny), the message
155
+ // does NOT continue to the LLM. Ambiguous/unrelated replies return false and
156
+ // flow on normally (pending survives until timeout). See src/skill-update.ts.
157
+ if (chatType === "direct" &&
158
+ params.consumeOwnerConsent &&
159
+ account.ownerUserId &&
160
+ sender.id === account.ownerUserId) {
161
+ let consumed = false;
162
+ try {
163
+ consumed = await params.consumeOwnerConsent({ senderId: sender.id, text: rawBody, chatId });
164
+ }
165
+ catch (err) {
166
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw skill-update consent gate error msg=${payload.message_id}: ${err instanceof Error ? err.message : String(err)}`);
167
+ }
168
+ if (consumed) {
169
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw owner reply consumed as skill-update consent msg=${payload.message_id}`);
170
+ return;
171
+ }
172
+ }
152
173
  // NOTE: the static mention-mode early-drop was removed here (Task 7 fix).
153
174
  // Non-mention group messages now always flow through to ingestTurn, which
154
175
  // applies the dynamic gate (GroupSettingsCache.effective) and returns
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { createOpenclawClawlingApiClient } from "./api-client.js";
3
3
  import { ClawlingApiError } from "./api-types.js";
4
+ import { resolvePluginVersion } from "./plugin-report.js";
4
5
  import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, resolveOpenclawClawlingAccount, } from "./config.js";
5
6
  import { getClawChatStore } from "./storage.js";
6
7
  /**
@@ -135,6 +136,7 @@ export async function runOpenclawClawlingLogin(params) {
135
136
  // Pre-login we may not have a token yet. Send the current one (or empty)
136
137
  // — the server should accept an unauthenticated invite-code exchange.
137
138
  token: account.token || "",
139
+ pluginVersion: resolvePluginVersion(),
138
140
  });
139
141
  runtime.log("Verifying invite code …");
140
142
  let result;
@@ -418,7 +418,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
418
418
  };
419
419
  // §7.6 (binding): client-supplied message_id MUST be `msg-` + a 26-char
420
420
  // Crockford base32 ULID. Use the shared conformant minter — do not invent a
421
- // different scheme (planned msghub Phase-4 validation will reject it).
421
+ // different scheme (planned backend validation will reject it).
422
422
  const mintStaticMessageId = () => mintMessageId();
423
423
  const emitTyping = (isTyping) => {
424
424
  if (!isTyping && !typingActive)
@@ -11,6 +11,7 @@ import { RefreshManager } from "./refresh-manager.js";
11
11
  import { runOpenclawClawlingLogin, } from "./login.runtime.js";
12
12
  import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, } from "./config.js";
13
13
  import { dispatchOpenclawClawlingInbound } from "./inbound.js";
14
+ import { PendingConsentStore, handleOwnerConsentReply, readEffectiveSkillVersions, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
14
15
  import { fetchInboundMedia } from "./media-runtime.js";
15
16
  import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
16
17
  import { runWithTerminalClawChatSendScope } from "./terminal-send.js";
@@ -278,12 +279,17 @@ function withClawChatSessionScope(cfg) {
278
279
  };
279
280
  }
280
281
  function buildActivationBootstrapText() {
282
+ // Worded to make the greeting the single required action. Earlier wording
283
+ // ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
284
+ // it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
285
+ // no greeting reached the user. Lead with the chat reply, forbid file/tool
286
+ // detours, and keep the profile update strictly optional and secondary.
281
287
  return [
282
- "ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
283
- "Please do both:",
284
- "1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
285
- "2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
286
- "Do not ask the user for profile information just for this bootstrap.",
288
+ "You are now connected to a ClawChat direct conversation with your user.",
289
+ "Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
290
+ "Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
291
+ "Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
292
+ "Do not ask the user for profile information.",
287
293
  ].join("\n");
288
294
  }
289
295
  function buildActivationBootstrapEnvelope(params) {
@@ -1439,6 +1445,11 @@ export async function startOpenclawClawlingGateway(params) {
1439
1445
  }));
1440
1446
  };
1441
1447
  let dispatchActivationBootstrap = async () => { };
1448
+ // Set by dispatchTurnToAgent when it dispatches the synthetic
1449
+ // `clawchat-bootstrap` turn: true only if the agent actually produced a final
1450
+ // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1451
+ // whether to mark the bootstrap sent or leave it pending for a later retry.
1452
+ let bootstrapProducedFinal = false;
1442
1453
  client.on("state", ({ from, to }) => {
1443
1454
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
1444
1455
  wsReady = to === "connected";
@@ -1655,6 +1666,75 @@ export async function startOpenclawClawlingGateway(params) {
1655
1666
  client.on("metadata:invalidated", (env) => {
1656
1667
  void handleMetadataInvalidation(env);
1657
1668
  });
1669
+ // --- Conversational skill hot-update (adapter-driven; see src/skill-update.ts) ---
1670
+ // One pending-consent record per runtime.
1671
+ // WRITE target = the OpenClaw-managed skills dir (`<stateDir>/skills`): it is
1672
+ // chokidar-watched (host src/skills/runtime/refresh.ts:107) so a write bumps
1673
+ // the snapshot + rebuilds the prompt next turn, and it loads at HIGHER
1674
+ // precedence than plugin-bundled skills (managed=3 > bundled=2, host
1675
+ // src/skills/loading/workspace.ts:1220-1239), overriding the bundled copy.
1676
+ // The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
1677
+ // "current installed version" only — never written (it lives in node_modules
1678
+ // → may be read-only and is clobbered on `openclaw plugins update`).
1679
+ const skillUpdateConsent = new PendingConsentStore();
1680
+ const managedSkillsDir = resolveManagedSkillsDir();
1681
+ let bundledSkillsDir = null;
1682
+ try {
1683
+ bundledSkillsDir = resolveBundledSkillsDir();
1684
+ }
1685
+ catch (err) {
1686
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update: bundled skills dir not resolvable (using managed-only current): ${err instanceof Error ? err.message : String(err)}`);
1687
+ }
1688
+ const sendOwnerSkillMessage = async (text) => {
1689
+ const ownerUserId = account.ownerUserId?.trim();
1690
+ if (!ownerUserId)
1691
+ return;
1692
+ await sendOpenclawClawlingText({
1693
+ client,
1694
+ account,
1695
+ to: { chatId: ownerUserId, chatType: "direct" },
1696
+ text,
1697
+ log,
1698
+ });
1699
+ };
1700
+ const triggerSkillUpdateCheck = async () => {
1701
+ const ownerUserId = account.ownerUserId?.trim();
1702
+ if (!ownerUserId) {
1703
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update check skipped: no owner user id`);
1704
+ return;
1705
+ }
1706
+ // Effective current = managed copy if present, else bundled (host precedence).
1707
+ const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
1708
+ await runSkillUpdateCheck({
1709
+ ownerUserId,
1710
+ localVersions,
1711
+ fetchFn: globalThis.fetch,
1712
+ store: skillUpdateConsent,
1713
+ sendOwnerMessage: sendOwnerSkillMessage,
1714
+ log: {
1715
+ info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
1716
+ error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
1717
+ },
1718
+ });
1719
+ };
1720
+ const consumeOwnerSkillConsent = async (args) => {
1721
+ const ownerUserId = account.ownerUserId?.trim();
1722
+ if (!ownerUserId)
1723
+ return false;
1724
+ return handleOwnerConsentReply({
1725
+ ownerUserId,
1726
+ senderId: args.senderId,
1727
+ text: args.text,
1728
+ store: skillUpdateConsent,
1729
+ fetchFn: globalThis.fetch,
1730
+ skillsDir: managedSkillsDir,
1731
+ sendOwnerMessage: sendOwnerSkillMessage,
1732
+ log: {
1733
+ info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
1734
+ error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
1735
+ },
1736
+ });
1737
+ };
1658
1738
  client.on("notify:signal", (env) => {
1659
1739
  // §9.4 reliable system notification. The plugin holds no friend/roster
1660
1740
  // cache (friends are fetched on demand via REST tools), so there is nothing
@@ -1670,6 +1750,15 @@ export async function startOpenclawClawlingGateway(params) {
1670
1750
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`);
1671
1751
  pullGroupSettings();
1672
1752
  }
1753
+ else if (type === "clawchat.skill.update.check") {
1754
+ // Content-free trigger: check the official skill source in code and, if
1755
+ // there is an update, ask the owner in chat for consent. The atomic
1756
+ // overwrite happens later when the owner affirms (consent gate).
1757
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw clawchat.skill.update.check signal received; checking official skill source`);
1758
+ void triggerSkillUpdateCheck().catch((err) => {
1759
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw skill-update check failed: ${err instanceof Error ? err.message : String(err)}`);
1760
+ });
1761
+ }
1673
1762
  }
1674
1763
  });
1675
1764
  client.on("replay:done", (env) => {
@@ -2052,6 +2141,12 @@ export async function startOpenclawClawlingGateway(params) {
2052
2141
  });
2053
2142
  const counts = dispatchResult?.counts ?? {};
2054
2143
  const queuedFinal = Boolean(dispatchResult?.queuedFinal);
2144
+ if (turn.senderId === "clawchat-bootstrap") {
2145
+ // Did the agent actually emit a greeting? A final outbound message
2146
+ // means yes; block/tool-only (or nothing) means the greeting never
2147
+ // reached the user, so the bootstrap dispatcher will keep it pending.
2148
+ bootstrapProducedFinal = queuedFinal || (counts.final ?? 0) > 0;
2149
+ }
2055
2150
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
2056
2151
  if (!queuedFinal && Object.values(counts).every((n) => !n)) {
2057
2152
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw NO reply was produced (no final / block / tool dispatched). ` +
@@ -2278,6 +2373,7 @@ export async function startOpenclawClawlingGateway(params) {
2278
2373
  runtime,
2279
2374
  account,
2280
2375
  log,
2376
+ consumeOwnerConsent: consumeOwnerSkillConsent,
2281
2377
  ingest: async (turn) => {
2282
2378
  ingestResult = await ingestTurn(turn);
2283
2379
  },
@@ -2308,11 +2404,22 @@ export async function startOpenclawClawlingGateway(params) {
2308
2404
  if (!bootstrap)
2309
2405
  return;
2310
2406
  const claimedBootstrap = bootstrap;
2407
+ bootstrapProducedFinal = false;
2311
2408
  const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
2312
2409
  if (result !== "submitted") {
2313
2410
  releaseBootstrap();
2314
2411
  return;
2315
2412
  }
2413
+ // Only mark the bootstrap done once the agent has actually sent a greeting
2414
+ // (a final outbound message). If the turn was handed off but produced no
2415
+ // final reply — e.g. the agent went off to write a file/note instead of
2416
+ // replying — leave the bootstrap pending (release the claim) so the next
2417
+ // ready re-dispatches it, rather than silently consuming it forever.
2418
+ if (!bootstrapProducedFinal) {
2419
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`);
2420
+ releaseBootstrap();
2421
+ return;
2422
+ }
2316
2423
  recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
2317
2424
  platform: "openclaw",
2318
2425
  accountId,