@clawling/clawchat-plugin-openclaw 2026.7.2 → 2026.7.6-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/dist/src/activation-greeting.js +34 -0
- package/dist/src/api-client.js +1 -0
- package/dist/src/config.js +1 -1
- package/dist/src/plugin-report.js +1 -0
- package/dist/src/refresh-manager.js +19 -0
- package/dist/src/runtime.js +99 -33
- package/dist/src/skill-update.js +139 -69
- package/package.json +1 -1
- package/src/activation-greeting.ts +35 -0
- package/src/api-client.ts +2 -0
- package/src/config.ts +1 -1
- package/src/plugin-report.ts +2 -0
- package/src/refresh-manager.ts +21 -0
- package/src/runtime.ts +103 -34
- package/src/skill-update.ts +170 -85
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// Worded to make the greeting the single required action. Earlier wording
|
|
6
|
+
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
7
|
+
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
8
|
+
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
9
|
+
// detours, and keep the profile update strictly optional and secondary.
|
|
10
|
+
const ACTIVATION_BOOTSTRAP_FALLBACK = [
|
|
11
|
+
"You are now connected to a ClawChat direct conversation with your user.",
|
|
12
|
+
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
13
|
+
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
14
|
+
"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.",
|
|
15
|
+
"Do not ask the user for profile information.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
|
|
18
|
+
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
19
|
+
// first-load without a restart. Any read failure falls back to the built-in text.
|
|
20
|
+
export function buildActivationBootstrapText(homeDir: string = os.homedir()): string {
|
|
21
|
+
const greetingPath = path.join(homeDir, "clawchat", "greeting.md");
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(greetingPath);
|
|
24
|
+
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
25
|
+
if (override.length > 0) {
|
|
26
|
+
return override;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
30
|
+
if (code !== "ENOENT") {
|
|
31
|
+
console.warn(`clawchat.greeting failed to read override ${greetingPath}:`, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return ACTIVATION_BOOTSTRAP_FALLBACK;
|
|
35
|
+
}
|
package/src/api-client.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface PluginReportInput {
|
|
|
24
24
|
deviceId: string;
|
|
25
25
|
platform: string;
|
|
26
26
|
pluginVersion: string;
|
|
27
|
+
agentVersion: string;
|
|
27
28
|
runtimeName: string;
|
|
28
29
|
runtimeVersion: string;
|
|
29
30
|
}
|
|
@@ -33,6 +34,7 @@ export function buildPluginReportBody(input: PluginReportInput): Record<string,
|
|
|
33
34
|
device_id: input.deviceId,
|
|
34
35
|
platform: input.platform,
|
|
35
36
|
plugin_version: input.pluginVersion,
|
|
37
|
+
agent_version: input.agentVersion,
|
|
36
38
|
runtime_name: input.runtimeName,
|
|
37
39
|
runtime_version: input.runtimeVersion,
|
|
38
40
|
};
|
package/src/config.ts
CHANGED
|
@@ -341,7 +341,7 @@ function decodeJwtStringClaim(token: string, claim: string): string {
|
|
|
341
341
|
* userId that disagrees with `sub` is always wrong for the session this token
|
|
342
342
|
* creates, and it silently breaks self-echo detection: with the OWNER's id
|
|
343
343
|
* here, every owner message is dropped as the bot's own echo (2026-07-02
|
|
344
|
-
*
|
|
344
|
+
* production incident — a provisioner wrote the owner id into the config). Token
|
|
345
345
|
* wins; the mismatch is logged. Also derives userId from `sub` when nothing is
|
|
346
346
|
* configured, mirroring the `oid` fallback above.
|
|
347
347
|
*/
|
package/src/plugin-report.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface ReportParams {
|
|
|
46
46
|
token: string;
|
|
47
47
|
deviceId: string;
|
|
48
48
|
pluginVersion: string;
|
|
49
|
+
agentVersion: string;
|
|
49
50
|
authenticated: boolean;
|
|
50
51
|
log?: { debug?: (msg: string) => void };
|
|
51
52
|
}
|
|
@@ -66,6 +67,7 @@ export async function reportPluginVersionSafe(p: ReportParams): Promise<void> {
|
|
|
66
67
|
deviceId: p.deviceId,
|
|
67
68
|
platform: "openclaw",
|
|
68
69
|
pluginVersion: p.pluginVersion,
|
|
70
|
+
agentVersion: p.agentVersion,
|
|
69
71
|
runtimeName: "node",
|
|
70
72
|
runtimeVersion: process.version,
|
|
71
73
|
},
|
package/src/refresh-manager.ts
CHANGED
|
@@ -32,6 +32,13 @@ export const MIN_REFRESH_INTERVAL_MS = 30_000;
|
|
|
32
32
|
export const PROACTIVE_JITTER_MS = 5 * MINUTE_MS;
|
|
33
33
|
/** §A.0 — fallback access-token TTL when `exp` is unparseable. */
|
|
34
34
|
export const ACCESS_TOKEN_TTL_MS = 24 * HOUR_MS;
|
|
35
|
+
/**
|
|
36
|
+
* §A.1 — Node stores a `setTimeout` delay in a signed 32-bit int; a delay above
|
|
37
|
+
* this silently clamps to 1ms and fires *immediately*. Long-lived (~30-day)
|
|
38
|
+
* access tokens push `refresh_at - now` well past this, so the proactive wait is
|
|
39
|
+
* chunked into at-most-this-long sleeps that re-arm until the real time.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647; // 2^31 - 1
|
|
35
42
|
|
|
36
43
|
/** Opaque timer handle — `setTimeout` returns a `Timeout` in node, a number in others. */
|
|
37
44
|
export type TimerHandle = ReturnType<typeof setTimeout> | number;
|
|
@@ -296,6 +303,20 @@ export class RefreshManager {
|
|
|
296
303
|
jitterMs,
|
|
297
304
|
});
|
|
298
305
|
const delayMs = Math.max(0, refreshAtMs - this.now());
|
|
306
|
+
if (delayMs > MAX_TIMER_DELAY_MS) {
|
|
307
|
+
// Too far out for a single setTimeout without 32-bit overflow (which would
|
|
308
|
+
// fire at 1ms and trigger an immediate refresh + WS reconnect, cancelling
|
|
309
|
+
// any in-flight send). Sleep one max chunk, then re-arm from the still-live
|
|
310
|
+
// token for the remaining wait.
|
|
311
|
+
this.ports.log?.debug?.(
|
|
312
|
+
`clawchat-plugin-openclaw proactive timer chunked delayMs=${delayMs} chunk=${MAX_TIMER_DELAY_MS}`,
|
|
313
|
+
);
|
|
314
|
+
this.proactiveTimer = this.setTimer(() => {
|
|
315
|
+
this.proactiveTimer = null;
|
|
316
|
+
this.armProactiveTimer(activatedAtMs);
|
|
317
|
+
}, MAX_TIMER_DELAY_MS);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
299
320
|
this.ports.log?.debug?.(
|
|
300
321
|
`clawchat-plugin-openclaw proactive timer armed delayMs=${delayMs}`,
|
|
301
322
|
);
|
package/src/runtime.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
15
15
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
16
16
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
17
17
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
|
+
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
18
19
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
19
20
|
import { ensureLivewareCli } from "./liveware-cli.ts";
|
|
20
21
|
import { ClawlingApiError } from "./api-types.ts";
|
|
@@ -38,7 +39,8 @@ import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inboun
|
|
|
38
39
|
import {
|
|
39
40
|
PendingConsentStore,
|
|
40
41
|
handleOwnerConsentReply,
|
|
41
|
-
|
|
42
|
+
managedSkillExists,
|
|
43
|
+
readLocalSkillState,
|
|
42
44
|
resolveBundledSkillsDir,
|
|
43
45
|
resolveManagedSkillsDir,
|
|
44
46
|
runSkillUpdateCheck,
|
|
@@ -100,6 +102,7 @@ type RuntimeConnectionStore = Pick<
|
|
|
100
102
|
Pick<
|
|
101
103
|
ClawChatStore,
|
|
102
104
|
| "getActivationCredentials"
|
|
105
|
+
| "upsertActivation"
|
|
103
106
|
| "rotateActivationTokens"
|
|
104
107
|
| "clearActivationCredentials"
|
|
105
108
|
| "insertMessage"
|
|
@@ -455,21 +458,6 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
455
458
|
};
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
function buildActivationBootstrapText(): string {
|
|
459
|
-
// Worded to make the greeting the single required action. Earlier wording
|
|
460
|
-
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
461
|
-
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
462
|
-
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
463
|
-
// detours, and keep the profile update strictly optional and secondary.
|
|
464
|
-
return [
|
|
465
|
-
"You are now connected to a ClawChat direct conversation with your user.",
|
|
466
|
-
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
467
|
-
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
468
|
-
"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.",
|
|
469
|
-
"Do not ask the user for profile information.",
|
|
470
|
-
].join("\n");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
461
|
function buildActivationBootstrapEnvelope(params: {
|
|
474
462
|
account: ResolvedOpenclawClawlingAccount;
|
|
475
463
|
conversationId: string;
|
|
@@ -871,6 +859,28 @@ async function waitForActivationCredentials(params: {
|
|
|
871
859
|
}): Promise<{ account: ResolvedOpenclawClawlingAccount; source: "configured" | "sqlite" } | null> {
|
|
872
860
|
const { abortSignal, getStatus, setStatus, store, log } = params;
|
|
873
861
|
const accountId = params.account.accountId;
|
|
862
|
+
// SQLite-first: activation and token rotation persist to the activations
|
|
863
|
+
// row, so a complete row is the durable credential source of truth even
|
|
864
|
+
// when channel config/env also carries credentials. Config/env credentials
|
|
865
|
+
// only bootstrap the no-row case (the caller seeds a row from them).
|
|
866
|
+
if (store?.getActivationCredentials) {
|
|
867
|
+
const stored = store.getActivationCredentials({ platform: "openclaw", accountId });
|
|
868
|
+
if (stored && stored.accessToken !== params.rejectedActivationToken) {
|
|
869
|
+
log?.info?.(
|
|
870
|
+
`[${accountId}] clawchat-plugin-openclaw loaded activation credentials from sqlite`,
|
|
871
|
+
);
|
|
872
|
+
return {
|
|
873
|
+
source: "sqlite",
|
|
874
|
+
account: {
|
|
875
|
+
...params.account,
|
|
876
|
+
configured: true,
|
|
877
|
+
token: stored.accessToken,
|
|
878
|
+
userId: stored.userId,
|
|
879
|
+
ownerUserId: stored.ownerUserId,
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
874
884
|
if (accountHasConnectCredentials(params.account)) {
|
|
875
885
|
return { account: params.account, source: "configured" };
|
|
876
886
|
}
|
|
@@ -956,12 +966,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
956
966
|
// so the backend links both to the same row. Imported from ./client.ts.
|
|
957
967
|
const reportDeviceId = resolveOpenclawClawlingDeviceId(account);
|
|
958
968
|
const pluginVersion = resolvePluginVersion();
|
|
969
|
+
const agentVersion = runtime.version ?? "";
|
|
959
970
|
void reportPluginVersionSafe({
|
|
960
971
|
baseUrl: account.baseUrl,
|
|
961
972
|
mediaBaseUrl: account.mediaBaseUrl,
|
|
962
973
|
token: "",
|
|
963
974
|
deviceId: reportDeviceId,
|
|
964
975
|
pluginVersion,
|
|
976
|
+
agentVersion,
|
|
965
977
|
authenticated: false,
|
|
966
978
|
log,
|
|
967
979
|
});
|
|
@@ -1001,6 +1013,27 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1001
1013
|
});
|
|
1002
1014
|
if (!activationAccount) return;
|
|
1003
1015
|
account = activationAccount.account;
|
|
1016
|
+
// Config/env bootstrap seeding: converge on SQLite as the single durable
|
|
1017
|
+
// credential store, so token rotation always has a row to update and the
|
|
1018
|
+
// next startup resolves SQLite-first to the same credentials.
|
|
1019
|
+
if (activationAccount.source === "configured" && store?.upsertActivation) {
|
|
1020
|
+
try {
|
|
1021
|
+
store.upsertActivation({
|
|
1022
|
+
platform: "openclaw",
|
|
1023
|
+
accountId,
|
|
1024
|
+
userId: account.userId,
|
|
1025
|
+
ownerUserId: account.ownerUserId,
|
|
1026
|
+
accessToken: account.token,
|
|
1027
|
+
refreshToken: readConfigRefreshToken(cfg),
|
|
1028
|
+
loginMethod: "config-seed",
|
|
1029
|
+
deviceId: CHANNEL_ID,
|
|
1030
|
+
});
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
log?.error?.(
|
|
1033
|
+
`[${accountId}] clawchat-plugin-openclaw sqlite credential seed failed; continuing with config credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1004
1037
|
// Paired: link the report row via the authenticated endpoint, reusing the
|
|
1005
1038
|
// SAME frozen device_id so the backend upserts the existing unpaired row.
|
|
1006
1039
|
void reportPluginVersionSafe({
|
|
@@ -1009,12 +1042,15 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1009
1042
|
token: account.token,
|
|
1010
1043
|
deviceId: reportDeviceId,
|
|
1011
1044
|
pluginVersion,
|
|
1045
|
+
agentVersion,
|
|
1012
1046
|
authenticated: true,
|
|
1013
1047
|
log,
|
|
1014
1048
|
});
|
|
1015
|
-
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1049
|
+
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
|
1050
|
+
// configured start even though the seed block above may have just created a
|
|
1051
|
+
// row — this cell only ever queries `store.getActivationCredentials` when
|
|
1052
|
+
// `source === "sqlite"`, so a config-sourced account relies on the JWT `exp`
|
|
1053
|
+
// alone until its next SQLite-first resolution.
|
|
1018
1054
|
let activatedAtMs: number | null =
|
|
1019
1055
|
activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
1020
1056
|
? store.getActivationCredentials({ platform: "openclaw", accountId })?.activatedAt ?? null
|
|
@@ -1207,16 +1243,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1207
1243
|
getAccessToken: () => account.token,
|
|
1208
1244
|
getRefreshToken: () => latestRefreshToken,
|
|
1209
1245
|
persistRotatedTokens: async (tokens) => {
|
|
1210
|
-
// §0 — persist
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
// failure
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1246
|
+
// §0 — persist durably BEFORE the in-memory swap. SQLite is the
|
|
1247
|
+
// startup credential source of truth: when a store is available its
|
|
1248
|
+
// write must succeed or the refresh stays transient (no swap, keep
|
|
1249
|
+
// current tokens, back off). A 0-row UPDATE (`false`) means the
|
|
1250
|
+
// activations row is missing (e.g. cleared out-of-band) rather than a
|
|
1251
|
+
// write failure — self-heal by re-seeding it from the in-memory
|
|
1252
|
+
// identity with the ROTATED pair (mirrors Hermes's seed-on-rotate
|
|
1253
|
+
// §C.2 semantics), then continue exactly as on success. `null` is a
|
|
1254
|
+
// caught write exception and stays transient — that's a real failure,
|
|
1255
|
+
// not a missing row.
|
|
1220
1256
|
if (store?.rotateActivationTokens) {
|
|
1221
1257
|
const rotateResult = store.rotateActivationTokens({
|
|
1222
1258
|
platform: "openclaw",
|
|
@@ -1224,12 +1260,41 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1224
1260
|
accessToken: tokens.accessToken,
|
|
1225
1261
|
refreshToken: tokens.refreshToken,
|
|
1226
1262
|
});
|
|
1227
|
-
if (rotateResult ===
|
|
1263
|
+
if (rotateResult === false) {
|
|
1264
|
+
if (!store.upsertActivation) {
|
|
1265
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
store.upsertActivation({
|
|
1269
|
+
platform: "openclaw",
|
|
1270
|
+
accountId,
|
|
1271
|
+
userId: account.userId,
|
|
1272
|
+
ownerUserId: account.ownerUserId,
|
|
1273
|
+
accessToken: tokens.accessToken,
|
|
1274
|
+
refreshToken: tokens.refreshToken,
|
|
1275
|
+
loginMethod: "config-seed",
|
|
1276
|
+
deviceId: refreshDeviceId,
|
|
1277
|
+
});
|
|
1278
|
+
} catch {
|
|
1279
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1280
|
+
}
|
|
1281
|
+
} else if (rotateResult === null) {
|
|
1228
1282
|
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1229
1283
|
}
|
|
1284
|
+
// Channel config is a human-readable mirror once SQLite holds the
|
|
1285
|
+
// pair — sync best-effort, never fail the refresh over it.
|
|
1286
|
+
try {
|
|
1287
|
+
await persistConfigTokens(tokens);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
log?.error?.(
|
|
1290
|
+
`[${accountId}] clawchat-plugin-openclaw config token mirror failed (sqlite already rotated): ${error instanceof Error ? error.message : String(error)}`,
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1230
1294
|
}
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1295
|
+
// No SQLite store available — channel config is the only durable
|
|
1296
|
+
// store, so its write failure must reject and keep the refresh
|
|
1297
|
+
// transient (pre-existing behavior for store-less test transports).
|
|
1233
1298
|
await persistConfigTokens(tokens);
|
|
1234
1299
|
},
|
|
1235
1300
|
swapInMemoryTokens: (tokens) => {
|
|
@@ -2154,6 +2219,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2154
2219
|
// The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
|
|
2155
2220
|
// "current installed version" only — never written (it lives in node_modules
|
|
2156
2221
|
// → may be read-only and is clobbered on `openclaw plugins update`).
|
|
2222
|
+
// Update detection converges on raw sha256 (readLocalSkillState), not the
|
|
2223
|
+
// frontmatter version, so a same-version content fix still applies; a
|
|
2224
|
+
// manifest `removed` id tombstones the LOCAL managed copy (never bundled)
|
|
2225
|
+
// via removeManagedSkill, deleting only ids the official manifest names.
|
|
2157
2226
|
const skillUpdateConsent = new PendingConsentStore();
|
|
2158
2227
|
const managedSkillsDir = resolveManagedSkillsDir();
|
|
2159
2228
|
let bundledSkillsDir: string | null = null;
|
|
@@ -2203,10 +2272,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2203
2272
|
return;
|
|
2204
2273
|
}
|
|
2205
2274
|
// Effective current = managed copy if present, else bundled (host precedence).
|
|
2206
|
-
const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
|
|
2207
2275
|
await runSkillUpdateCheck({
|
|
2208
2276
|
ownerUserId,
|
|
2209
|
-
|
|
2277
|
+
readLocalState: (id) => readLocalSkillState(managedSkillsDir, bundledSkillsDir, id),
|
|
2278
|
+
hasManagedSkill: (id) => managedSkillExists(managedSkillsDir, id),
|
|
2210
2279
|
fetchFn: globalThis.fetch,
|
|
2211
2280
|
store: skillUpdateConsent,
|
|
2212
2281
|
sendOwnerMessage: sendOwnerSkillMessage,
|