@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 +11 -3
- package/dist/src/api-client.js +3 -0
- package/dist/src/inbound.js +21 -0
- package/dist/src/login.runtime.js +2 -0
- package/dist/src/reply-dispatcher.js +1 -1
- package/dist/src/runtime.js +112 -5
- package/dist/src/skill-update.js +559 -0
- package/dist/src/tools.js +1 -1
- package/dist/src/ws-client.js +1 -1
- package/package.json +18 -1
- package/skills/clawchat/SKILL.md +1 -0
- package/skills/liveware-app/SKILL.md +1 -0
- package/src/api-client.ts +13 -2
- package/src/inbound.ts +41 -0
- package/src/login.runtime.ts +2 -0
- package/src/reply-dispatcher.ts +1 -1
- package/src/runtime.ts +134 -5
- package/src/skill-update.ts +752 -0
- package/src/tools.ts +1 -1
- package/src/ws-client.ts +1 -1
package/src/inbound.ts
CHANGED
|
@@ -69,6 +69,18 @@ export interface DispatchInboundParams {
|
|
|
69
69
|
runtime: PluginRuntime;
|
|
70
70
|
account: ResolvedOpenclawClawlingAccount;
|
|
71
71
|
ingest: (params: IngestTurnParams) => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Pending-consent gate for conversational skill hot-update (see
|
|
74
|
+
* `src/skill-update.ts`). Called for direct messages from the owner BEFORE
|
|
75
|
+
* normal dispatch. Returns `true` when the reply was consumed as a consent
|
|
76
|
+
* decision (affirm/deny) and must NOT reach the LLM; `false` to let the
|
|
77
|
+
* message flow on normally (no pending update, or an ambiguous reply).
|
|
78
|
+
*/
|
|
79
|
+
consumeOwnerConsent?: (args: {
|
|
80
|
+
senderId: string;
|
|
81
|
+
text: string;
|
|
82
|
+
chatId: string;
|
|
83
|
+
}) => Promise<boolean>;
|
|
72
84
|
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
73
85
|
}
|
|
74
86
|
|
|
@@ -272,6 +284,35 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
272
284
|
userId: account.userId,
|
|
273
285
|
});
|
|
274
286
|
|
|
287
|
+
// Pending-consent gate for conversational skill hot-update. Only owner direct
|
|
288
|
+
// messages can answer a pending skill-update ask; the gate parses consent
|
|
289
|
+
// conservatively and, when it consumes the reply (affirm/deny), the message
|
|
290
|
+
// does NOT continue to the LLM. Ambiguous/unrelated replies return false and
|
|
291
|
+
// flow on normally (pending survives until timeout). See src/skill-update.ts.
|
|
292
|
+
if (
|
|
293
|
+
chatType === "direct" &&
|
|
294
|
+
params.consumeOwnerConsent &&
|
|
295
|
+
account.ownerUserId &&
|
|
296
|
+
sender.id === account.ownerUserId
|
|
297
|
+
) {
|
|
298
|
+
let consumed = false;
|
|
299
|
+
try {
|
|
300
|
+
consumed = await params.consumeOwnerConsent({ senderId: sender.id, text: rawBody, chatId });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
log?.error?.(
|
|
303
|
+
`[${account.accountId}] clawchat-plugin-openclaw skill-update consent gate error msg=${payload.message_id}: ${
|
|
304
|
+
err instanceof Error ? err.message : String(err)
|
|
305
|
+
}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (consumed) {
|
|
309
|
+
log?.info?.(
|
|
310
|
+
`[${account.accountId}] clawchat-plugin-openclaw owner reply consumed as skill-update consent msg=${payload.message_id}`,
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
275
316
|
// NOTE: the static mention-mode early-drop was removed here (Task 7 fix).
|
|
276
317
|
// Non-mention group messages now always flow through to ingestTurn, which
|
|
277
318
|
// applies the dynamic gate (GroupSettingsCache.effective) and returns
|
package/src/login.runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createInterface, type Interface as ReadlineInterface } from "node:readl
|
|
|
2
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
4
4
|
import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
|
|
5
|
+
import { resolvePluginVersion } from "./plugin-report.ts";
|
|
5
6
|
import {
|
|
6
7
|
CHANNEL_ID,
|
|
7
8
|
mergeOpenclawClawchatRuntimePluginActivation,
|
|
@@ -197,6 +198,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
197
198
|
// Pre-login we may not have a token yet. Send the current one (or empty)
|
|
198
199
|
// — the server should accept an unauthenticated invite-code exchange.
|
|
199
200
|
token: account.token || "",
|
|
201
|
+
pluginVersion: resolvePluginVersion(),
|
|
200
202
|
});
|
|
201
203
|
|
|
202
204
|
runtime.log("Verifying invite code …");
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -546,7 +546,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
546
546
|
|
|
547
547
|
// §7.6 (binding): client-supplied message_id MUST be `msg-` + a 26-char
|
|
548
548
|
// Crockford base32 ULID. Use the shared conformant minter — do not invent a
|
|
549
|
-
// different scheme (planned
|
|
549
|
+
// different scheme (planned backend validation will reject it).
|
|
550
550
|
const mintStaticMessageId = () => mintMessageId();
|
|
551
551
|
|
|
552
552
|
const emitTyping = (isTyping: boolean) => {
|
package/src/runtime.ts
CHANGED
|
@@ -35,6 +35,14 @@ import {
|
|
|
35
35
|
} from "./config.ts";
|
|
36
36
|
import type { ClawlingChatClient } from "./ws-client.ts";
|
|
37
37
|
import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
|
|
38
|
+
import {
|
|
39
|
+
PendingConsentStore,
|
|
40
|
+
handleOwnerConsentReply,
|
|
41
|
+
readEffectiveSkillVersions,
|
|
42
|
+
resolveBundledSkillsDir,
|
|
43
|
+
resolveManagedSkillsDir,
|
|
44
|
+
runSkillUpdateCheck,
|
|
45
|
+
} from "./skill-update.ts";
|
|
38
46
|
import { fetchInboundMedia } from "./media-runtime.ts";
|
|
39
47
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
|
|
40
48
|
import { runWithTerminalClawChatSendScope } from "./terminal-send.ts";
|
|
@@ -419,12 +427,17 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
419
427
|
}
|
|
420
428
|
|
|
421
429
|
function buildActivationBootstrapText(): string {
|
|
430
|
+
// Worded to make the greeting the single required action. Earlier wording
|
|
431
|
+
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
432
|
+
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
433
|
+
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
434
|
+
// detours, and keep the profile update strictly optional and secondary.
|
|
422
435
|
return [
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
"Do not ask the user for profile information
|
|
436
|
+
"You are now connected to a ClawChat direct conversation with your user.",
|
|
437
|
+
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
438
|
+
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
439
|
+
"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.",
|
|
440
|
+
"Do not ask the user for profile information.",
|
|
428
441
|
].join("\n");
|
|
429
442
|
}
|
|
430
443
|
|
|
@@ -1808,6 +1821,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1808
1821
|
);
|
|
1809
1822
|
};
|
|
1810
1823
|
let dispatchActivationBootstrap: () => Promise<void> = async () => {};
|
|
1824
|
+
// Set by dispatchTurnToAgent when it dispatches the synthetic
|
|
1825
|
+
// `clawchat-bootstrap` turn: true only if the agent actually produced a final
|
|
1826
|
+
// outbound message (the greeting). The bootstrap dispatcher reads it to decide
|
|
1827
|
+
// whether to mark the bootstrap sent or leave it pending for a later retry.
|
|
1828
|
+
let bootstrapProducedFinal = false;
|
|
1811
1829
|
|
|
1812
1830
|
client.on("state", ({ from, to }) => {
|
|
1813
1831
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
|
|
@@ -2038,6 +2056,83 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2038
2056
|
void handleMetadataInvalidation(env);
|
|
2039
2057
|
});
|
|
2040
2058
|
|
|
2059
|
+
// --- Conversational skill hot-update (adapter-driven; see src/skill-update.ts) ---
|
|
2060
|
+
// One pending-consent record per runtime.
|
|
2061
|
+
// WRITE target = the OpenClaw-managed skills dir (`<stateDir>/skills`): it is
|
|
2062
|
+
// chokidar-watched (host src/skills/runtime/refresh.ts:107) so a write bumps
|
|
2063
|
+
// the snapshot + rebuilds the prompt next turn, and it loads at HIGHER
|
|
2064
|
+
// precedence than plugin-bundled skills (managed=3 > bundled=2, host
|
|
2065
|
+
// src/skills/loading/workspace.ts:1220-1239), overriding the bundled copy.
|
|
2066
|
+
// The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
|
|
2067
|
+
// "current installed version" only — never written (it lives in node_modules
|
|
2068
|
+
// → may be read-only and is clobbered on `openclaw plugins update`).
|
|
2069
|
+
const skillUpdateConsent = new PendingConsentStore();
|
|
2070
|
+
const managedSkillsDir = resolveManagedSkillsDir();
|
|
2071
|
+
let bundledSkillsDir: string | null = null;
|
|
2072
|
+
try {
|
|
2073
|
+
bundledSkillsDir = resolveBundledSkillsDir();
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
log?.info?.(
|
|
2076
|
+
`[${accountId}] clawchat-plugin-openclaw skill-update: bundled skills dir not resolvable (using managed-only current): ${
|
|
2077
|
+
err instanceof Error ? err.message : String(err)
|
|
2078
|
+
}`,
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
const sendOwnerSkillMessage = async (text: string): Promise<void> => {
|
|
2082
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
2083
|
+
if (!ownerUserId) return;
|
|
2084
|
+
await sendOpenclawClawlingText({
|
|
2085
|
+
client,
|
|
2086
|
+
account,
|
|
2087
|
+
to: { chatId: ownerUserId, chatType: "direct" },
|
|
2088
|
+
text,
|
|
2089
|
+
log,
|
|
2090
|
+
});
|
|
2091
|
+
};
|
|
2092
|
+
const triggerSkillUpdateCheck = async (): Promise<void> => {
|
|
2093
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
2094
|
+
if (!ownerUserId) {
|
|
2095
|
+
log?.info?.(
|
|
2096
|
+
`[${accountId}] clawchat-plugin-openclaw skill-update check skipped: no owner user id`,
|
|
2097
|
+
);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
// Effective current = managed copy if present, else bundled (host precedence).
|
|
2101
|
+
const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
|
|
2102
|
+
await runSkillUpdateCheck({
|
|
2103
|
+
ownerUserId,
|
|
2104
|
+
localVersions,
|
|
2105
|
+
fetchFn: globalThis.fetch,
|
|
2106
|
+
store: skillUpdateConsent,
|
|
2107
|
+
sendOwnerMessage: sendOwnerSkillMessage,
|
|
2108
|
+
log: {
|
|
2109
|
+
info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
|
|
2110
|
+
error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
|
|
2111
|
+
},
|
|
2112
|
+
});
|
|
2113
|
+
};
|
|
2114
|
+
const consumeOwnerSkillConsent = async (args: {
|
|
2115
|
+
senderId: string;
|
|
2116
|
+
text: string;
|
|
2117
|
+
chatId: string;
|
|
2118
|
+
}): Promise<boolean> => {
|
|
2119
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
2120
|
+
if (!ownerUserId) return false;
|
|
2121
|
+
return handleOwnerConsentReply({
|
|
2122
|
+
ownerUserId,
|
|
2123
|
+
senderId: args.senderId,
|
|
2124
|
+
text: args.text,
|
|
2125
|
+
store: skillUpdateConsent,
|
|
2126
|
+
fetchFn: globalThis.fetch,
|
|
2127
|
+
skillsDir: managedSkillsDir,
|
|
2128
|
+
sendOwnerMessage: sendOwnerSkillMessage,
|
|
2129
|
+
log: {
|
|
2130
|
+
info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
|
|
2131
|
+
error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
|
|
2132
|
+
},
|
|
2133
|
+
});
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2041
2136
|
client.on("notify:signal", (env: Envelope) => {
|
|
2042
2137
|
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
2043
2138
|
// cache (friends are fetched on demand via REST tools), so there is nothing
|
|
@@ -2054,6 +2149,20 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2054
2149
|
`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`,
|
|
2055
2150
|
);
|
|
2056
2151
|
pullGroupSettings();
|
|
2152
|
+
} else if (type === "clawchat.skill.update.check") {
|
|
2153
|
+
// Content-free trigger: check the official skill source in code and, if
|
|
2154
|
+
// there is an update, ask the owner in chat for consent. The atomic
|
|
2155
|
+
// overwrite happens later when the owner affirms (consent gate).
|
|
2156
|
+
log?.info?.(
|
|
2157
|
+
`[${accountId}] clawchat-plugin-openclaw clawchat.skill.update.check signal received; checking official skill source`,
|
|
2158
|
+
);
|
|
2159
|
+
void triggerSkillUpdateCheck().catch((err) => {
|
|
2160
|
+
log?.error?.(
|
|
2161
|
+
`[${accountId}] clawchat-plugin-openclaw skill-update check failed: ${
|
|
2162
|
+
err instanceof Error ? err.message : String(err)
|
|
2163
|
+
}`,
|
|
2164
|
+
);
|
|
2165
|
+
});
|
|
2057
2166
|
}
|
|
2058
2167
|
}
|
|
2059
2168
|
});
|
|
@@ -2480,6 +2589,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2480
2589
|
const queuedFinal = Boolean(
|
|
2481
2590
|
(dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
|
|
2482
2591
|
);
|
|
2592
|
+
if (turn.senderId === "clawchat-bootstrap") {
|
|
2593
|
+
// Did the agent actually emit a greeting? A final outbound message
|
|
2594
|
+
// means yes; block/tool-only (or nothing) means the greeting never
|
|
2595
|
+
// reached the user, so the bootstrap dispatcher will keep it pending.
|
|
2596
|
+
bootstrapProducedFinal = queuedFinal || (counts.final ?? 0) > 0;
|
|
2597
|
+
}
|
|
2483
2598
|
log?.info?.(
|
|
2484
2599
|
`[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
|
|
2485
2600
|
);
|
|
@@ -2744,6 +2859,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2744
2859
|
runtime,
|
|
2745
2860
|
account,
|
|
2746
2861
|
log,
|
|
2862
|
+
consumeOwnerConsent: consumeOwnerSkillConsent,
|
|
2747
2863
|
ingest: async (turn) => {
|
|
2748
2864
|
ingestResult = await ingestTurn(turn);
|
|
2749
2865
|
},
|
|
@@ -2777,6 +2893,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2777
2893
|
);
|
|
2778
2894
|
if (!bootstrap) return;
|
|
2779
2895
|
const claimedBootstrap = bootstrap;
|
|
2896
|
+
bootstrapProducedFinal = false;
|
|
2780
2897
|
const result = await handleInboundEnvelope(
|
|
2781
2898
|
buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
|
|
2782
2899
|
);
|
|
@@ -2784,6 +2901,18 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2784
2901
|
releaseBootstrap();
|
|
2785
2902
|
return;
|
|
2786
2903
|
}
|
|
2904
|
+
// Only mark the bootstrap done once the agent has actually sent a greeting
|
|
2905
|
+
// (a final outbound message). If the turn was handed off but produced no
|
|
2906
|
+
// final reply — e.g. the agent went off to write a file/note instead of
|
|
2907
|
+
// replying — leave the bootstrap pending (release the claim) so the next
|
|
2908
|
+
// ready re-dispatches it, rather than silently consuming it forever.
|
|
2909
|
+
if (!bootstrapProducedFinal) {
|
|
2910
|
+
log?.info?.(
|
|
2911
|
+
`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`,
|
|
2912
|
+
);
|
|
2913
|
+
releaseBootstrap();
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2787
2916
|
recordConnection("activation bootstrap sent", () =>
|
|
2788
2917
|
store.markActivationBootstrapSent?.({
|
|
2789
2918
|
platform: "openclaw",
|