@clawling/clawchat-plugin-openclaw 2026.5.13-dev.1 → 2026.5.13-dev.3

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/index.js CHANGED
@@ -16,5 +16,12 @@ export default defineChannelPluginEntry({
16
16
  registerOpenclawClawlingCommands(api);
17
17
  registerClawChatPromptInjection(api);
18
18
  registerOpenclawClawlingTools(api);
19
+ // NOTE: the legacy-rename config migration is intentionally NOT registered
20
+ // here. The host's setup-migration runner only loads a plugin's SETUP source
21
+ // (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
22
+ // `register(api)` in "setup-only" mode; registrations made in `registerFull`
23
+ // (full/tool-discovery modes) are never collected for migrations, and this
24
+ // gated full-load path doesn't run for the rename scenario anyway. The
25
+ // migration is wired into setup-entry.ts instead.
19
26
  },
20
27
  });
@@ -1,3 +1,33 @@
1
1
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { CHANNEL_ID } from "./src/config.js";
2
3
  import { openclawClawlingSetupPlugin } from "./src/channel.setup.js";
3
- export default defineSetupPluginEntry(openclawClawlingSetupPlugin);
4
+ import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.js";
5
+ /**
6
+ * Host setup-source `register` hook.
7
+ *
8
+ * The OpenClaw setup-migration runner (`setup-registry`) loads this module as
9
+ * the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
10
+ * / `runtimeSetupEntry`), unwraps the default export, and — when that export is
11
+ * an object exposing a `register` function whose `id` matches the plugin id —
12
+ * calls `register(api)` in setup-only mode. This is the ONLY ungated path that
13
+ * collects config migrations for the plugin-rename scenario.
14
+ *
15
+ * Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
16
+ * `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
17
+ */
18
+ export function register(api) {
19
+ api.registerConfigMigration?.((config) => migrateLegacyClawChatChannelConfig(config));
20
+ }
21
+ /**
22
+ * Default export consumed by BOTH host loaders of this setup source:
23
+ * - the channel-setup loader unwraps `default` and reads `.plugin`
24
+ * (`defineSetupPluginEntry` yields `{ plugin }`);
25
+ * - the setup-migration runner unwraps `default` and reads `.register`,
26
+ * rejecting it unless `.id` matches the plugin id.
27
+ * So the default export must carry `plugin`, `id`, and `register` together.
28
+ */
29
+ export default {
30
+ ...defineSetupPluginEntry(openclawClawlingSetupPlugin),
31
+ id: CHANNEL_ID,
32
+ register,
33
+ };
@@ -0,0 +1,120 @@
1
+ import { CHANNEL_ID } from "./config.js";
2
+ /**
3
+ * Compatibility migration for the plugin rename (commit 260044f): the plugin
4
+ * `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
5
+ * `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
6
+ *
7
+ * Users upgrading from the old version have all their state — channel block
8
+ * (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
9
+ * `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
10
+ * id. The new code only reads the new id, so the channel silently fails to
11
+ * load. This migration moves the old-keyed state onto the new id.
12
+ *
13
+ * The function is PURE: it clones the input via `structuredClone` and never
14
+ * mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
15
+ * returns an equivalent config with `changes: []`.
16
+ */
17
+ export const LEGACY_CHANNEL_ID = "openclaw-clawchat";
18
+ export const TARGET_CHANNEL_ID = CHANNEL_ID;
19
+ const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
20
+ function isBlockedObjectKey(key) {
21
+ return BLOCKED_OBJECT_KEYS.has(key);
22
+ }
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function isNonEmptyRecord(value) {
27
+ return isRecord(value) && Object.keys(value).length > 0;
28
+ }
29
+ /**
30
+ * Copy keys from `source` into `target` only when they are absent/undefined in
31
+ * `target` (target is more current — never overwrite an existing value).
32
+ * Recurses into nested records. Skips prototype-pollution keys.
33
+ */
34
+ function mergeMissing(target, source) {
35
+ for (const [key, value] of Object.entries(source)) {
36
+ if (value === undefined || isBlockedObjectKey(key))
37
+ continue;
38
+ const existing = target[key];
39
+ if (existing === undefined) {
40
+ target[key] = value;
41
+ continue;
42
+ }
43
+ if (isRecord(existing) && isRecord(value))
44
+ mergeMissing(existing, value);
45
+ }
46
+ }
47
+ /** Replace `from` → `to` in a string array, preserving order and deduping. */
48
+ function replaceAndDedup(list, from, to) {
49
+ const out = [];
50
+ for (const raw of list) {
51
+ const value = raw === from ? to : raw;
52
+ if (!out.includes(value))
53
+ out.push(value);
54
+ }
55
+ return out;
56
+ }
57
+ export function migrateLegacyClawChatChannelConfig(config) {
58
+ const changes = [];
59
+ if (!isRecord(config))
60
+ return { config, changes };
61
+ const next = structuredClone(config);
62
+ // 1. channels — move/merge the old channel block onto the new id.
63
+ const channels = next.channels;
64
+ if (isRecord(channels)) {
65
+ const oldChannel = channels[LEGACY_CHANNEL_ID];
66
+ if (isNonEmptyRecord(oldChannel)) {
67
+ const newChannel = channels[TARGET_CHANNEL_ID];
68
+ if (!isNonEmptyRecord(newChannel)) {
69
+ channels[TARGET_CHANNEL_ID] = oldChannel;
70
+ changes.push(`Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
71
+ }
72
+ else {
73
+ mergeMissing(newChannel, oldChannel);
74
+ changes.push(`Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`);
75
+ }
76
+ delete channels[LEGACY_CHANNEL_ID];
77
+ }
78
+ }
79
+ // plugins.* live under a single `plugins` record.
80
+ const plugins = next.plugins;
81
+ if (isRecord(plugins)) {
82
+ // 2. plugins.allow — replace old id with new id (append if missing), dedup.
83
+ const allow = plugins.allow;
84
+ if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
85
+ const replaced = replaceAndDedup(allow.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
86
+ plugins.allow = replaced;
87
+ changes.push(`Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
88
+ }
89
+ // 3. plugins.entries — merge-missing the old entry into the new one.
90
+ const entries = plugins.entries;
91
+ if (isRecord(entries)) {
92
+ const oldEntry = entries[LEGACY_CHANNEL_ID];
93
+ if (oldEntry !== undefined) {
94
+ if (isRecord(oldEntry)) {
95
+ const newEntry = entries[TARGET_CHANNEL_ID];
96
+ if (!isRecord(newEntry)) {
97
+ entries[TARGET_CHANNEL_ID] = oldEntry;
98
+ }
99
+ else {
100
+ mergeMissing(newEntry, oldEntry);
101
+ }
102
+ }
103
+ delete entries[LEGACY_CHANNEL_ID];
104
+ changes.push(`Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`);
105
+ }
106
+ }
107
+ }
108
+ // 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
109
+ const tools = next.tools;
110
+ if (isRecord(tools)) {
111
+ for (const key of ["allow", "alsoAllow"]) {
112
+ const list = tools[key];
113
+ if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
114
+ tools[key] = replaceAndDedup(list.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
115
+ changes.push(`Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
116
+ }
117
+ }
118
+ }
119
+ return { config: next, changes };
120
+ }
@@ -1610,7 +1610,13 @@ export async function startOpenclawClawlingGateway(params) {
1610
1610
  : {}),
1611
1611
  },
1612
1612
  ...(memoryRoot ? { extra: { memoryRoot } } : {}),
1613
- ...(turn.peer.kind === "group"
1613
+ // Deliver the rendered ClawChat per-turn prompt (owner agent_behavior,
1614
+ // metadata, peer/sender profile) to the host for ALL chat types. The host
1615
+ // appends `GroupSystemPrompt` to the system prompt regardless of chat
1616
+ // kind. Direct chats previously relied only on the `before_prompt_build`
1617
+ // staging hook, which is not applied by the host for DM sessions, so the
1618
+ // owner-configured behavior never reached the LLM in 1:1 chats.
1619
+ ...(turnPrompt
1614
1620
  ? { supplemental: { groupSystemPrompt: turnPrompt } }
1615
1621
  : {}),
1616
1622
  });
package/index.ts CHANGED
@@ -20,5 +20,12 @@ export default defineChannelPluginEntry({
20
20
  registerOpenclawClawlingCommands(api);
21
21
  registerClawChatPromptInjection(api as unknown as ClawChatPromptInjectionApi);
22
22
  registerOpenclawClawlingTools(api);
23
+ // NOTE: the legacy-rename config migration is intentionally NOT registered
24
+ // here. The host's setup-migration runner only loads a plugin's SETUP source
25
+ // (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
26
+ // `register(api)` in "setup-only" mode; registrations made in `registerFull`
27
+ // (full/tool-discovery modes) are never collected for migrations, and this
28
+ // gated full-load path doesn't run for the rename scenario anyway. The
29
+ // migration is wired into setup-entry.ts instead.
23
30
  },
24
31
  });
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "id": "clawchat-plugin-openclaw",
3
3
  "kind": "channel",
4
+ "legacyPluginIds": ["openclaw-clawchat"],
5
+ "configContracts": {
6
+ "compatibilityMigrationPaths": [
7
+ "channels.openclaw-clawchat",
8
+ "plugins.entries.openclaw-clawchat"
9
+ ]
10
+ },
4
11
  "channels": ["clawchat-plugin-openclaw"],
5
12
  "skills": ["./skills"],
6
13
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.5.13-dev.1",
3
+ "version": "2026.5.13-dev.3",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "files": [
package/setup-entry.ts CHANGED
@@ -1,4 +1,54 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
1
2
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
3
+ import { CHANNEL_ID } from "./src/config.ts";
2
4
  import { openclawClawlingSetupPlugin } from "./src/channel.setup.ts";
5
+ import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.ts";
3
6
 
4
- export default defineSetupPluginEntry(openclawClawlingSetupPlugin);
7
+ /**
8
+ * Minimal shape of the host setup-registration api we touch. The host calls
9
+ * `register(api)` on the SETUP source (this module) in `registrationMode:
10
+ * "setup-only"`; `registerConfigMigration` is the hook that collects
11
+ * compatibility migrations (run by `openclaw doctor` / setup). It is optional
12
+ * here so a host that doesn't expose it can't throw.
13
+ */
14
+ interface SetupRegisterApi {
15
+ registerConfigMigration?: (
16
+ migration: (config: OpenClawConfig) => {
17
+ config: OpenClawConfig;
18
+ changes: string[];
19
+ },
20
+ ) => void;
21
+ }
22
+
23
+ /**
24
+ * Host setup-source `register` hook.
25
+ *
26
+ * The OpenClaw setup-migration runner (`setup-registry`) loads this module as
27
+ * the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
28
+ * / `runtimeSetupEntry`), unwraps the default export, and — when that export is
29
+ * an object exposing a `register` function whose `id` matches the plugin id —
30
+ * calls `register(api)` in setup-only mode. This is the ONLY ungated path that
31
+ * collects config migrations for the plugin-rename scenario.
32
+ *
33
+ * Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
34
+ * `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
35
+ */
36
+ export function register(api: SetupRegisterApi): void {
37
+ api.registerConfigMigration?.((config) =>
38
+ migrateLegacyClawChatChannelConfig(config),
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Default export consumed by BOTH host loaders of this setup source:
44
+ * - the channel-setup loader unwraps `default` and reads `.plugin`
45
+ * (`defineSetupPluginEntry` yields `{ plugin }`);
46
+ * - the setup-migration runner unwraps `default` and reads `.register`,
47
+ * rejecting it unless `.id` matches the plugin id.
48
+ * So the default export must carry `plugin`, `id`, and `register` together.
49
+ */
50
+ export default {
51
+ ...defineSetupPluginEntry(openclawClawlingSetupPlugin),
52
+ id: CHANNEL_ID,
53
+ register,
54
+ };
@@ -0,0 +1,154 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
+ import { CHANNEL_ID } from "./config.ts";
3
+
4
+ /**
5
+ * Compatibility migration for the plugin rename (commit 260044f): the plugin
6
+ * `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
7
+ * `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
8
+ *
9
+ * Users upgrading from the old version have all their state — channel block
10
+ * (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
11
+ * `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
12
+ * id. The new code only reads the new id, so the channel silently fails to
13
+ * load. This migration moves the old-keyed state onto the new id.
14
+ *
15
+ * The function is PURE: it clones the input via `structuredClone` and never
16
+ * mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
17
+ * returns an equivalent config with `changes: []`.
18
+ */
19
+
20
+ export const LEGACY_CHANNEL_ID = "openclaw-clawchat" as const;
21
+ export const TARGET_CHANNEL_ID = CHANNEL_ID;
22
+
23
+ const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
24
+
25
+ function isBlockedObjectKey(key: string): boolean {
26
+ return BLOCKED_OBJECT_KEYS.has(key);
27
+ }
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ function isNonEmptyRecord(value: unknown): value is Record<string, unknown> {
34
+ return isRecord(value) && Object.keys(value).length > 0;
35
+ }
36
+
37
+ /**
38
+ * Copy keys from `source` into `target` only when they are absent/undefined in
39
+ * `target` (target is more current — never overwrite an existing value).
40
+ * Recurses into nested records. Skips prototype-pollution keys.
41
+ */
42
+ function mergeMissing(
43
+ target: Record<string, unknown>,
44
+ source: Record<string, unknown>,
45
+ ): void {
46
+ for (const [key, value] of Object.entries(source)) {
47
+ if (value === undefined || isBlockedObjectKey(key)) continue;
48
+ const existing = target[key];
49
+ if (existing === undefined) {
50
+ target[key] = value;
51
+ continue;
52
+ }
53
+ if (isRecord(existing) && isRecord(value)) mergeMissing(existing, value);
54
+ }
55
+ }
56
+
57
+ /** Replace `from` → `to` in a string array, preserving order and deduping. */
58
+ function replaceAndDedup(list: string[], from: string, to: string): string[] {
59
+ const out: string[] = [];
60
+ for (const raw of list) {
61
+ const value = raw === from ? to : raw;
62
+ if (!out.includes(value)) out.push(value);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function migrateLegacyClawChatChannelConfig(config: OpenClawConfig): {
68
+ config: OpenClawConfig;
69
+ changes: string[];
70
+ } {
71
+ const changes: string[] = [];
72
+ if (!isRecord(config)) return { config, changes };
73
+
74
+ const next = structuredClone(config) as Record<string, unknown>;
75
+
76
+ // 1. channels — move/merge the old channel block onto the new id.
77
+ const channels = next.channels;
78
+ if (isRecord(channels)) {
79
+ const oldChannel = channels[LEGACY_CHANNEL_ID];
80
+ if (isNonEmptyRecord(oldChannel)) {
81
+ const newChannel = channels[TARGET_CHANNEL_ID];
82
+ if (!isNonEmptyRecord(newChannel)) {
83
+ channels[TARGET_CHANNEL_ID] = oldChannel;
84
+ changes.push(
85
+ `Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
86
+ );
87
+ } else {
88
+ mergeMissing(newChannel, oldChannel);
89
+ changes.push(
90
+ `Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`,
91
+ );
92
+ }
93
+ delete channels[LEGACY_CHANNEL_ID];
94
+ }
95
+ }
96
+
97
+ // plugins.* live under a single `plugins` record.
98
+ const plugins = next.plugins;
99
+ if (isRecord(plugins)) {
100
+ // 2. plugins.allow — replace old id with new id (append if missing), dedup.
101
+ const allow = plugins.allow;
102
+ if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
103
+ const replaced = replaceAndDedup(
104
+ allow.filter((v): v is string => typeof v === "string"),
105
+ LEGACY_CHANNEL_ID,
106
+ TARGET_CHANNEL_ID,
107
+ );
108
+ plugins.allow = replaced;
109
+ changes.push(
110
+ `Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
111
+ );
112
+ }
113
+
114
+ // 3. plugins.entries — merge-missing the old entry into the new one.
115
+ const entries = plugins.entries;
116
+ if (isRecord(entries)) {
117
+ const oldEntry = entries[LEGACY_CHANNEL_ID];
118
+ if (oldEntry !== undefined) {
119
+ if (isRecord(oldEntry)) {
120
+ const newEntry = entries[TARGET_CHANNEL_ID];
121
+ if (!isRecord(newEntry)) {
122
+ entries[TARGET_CHANNEL_ID] = oldEntry;
123
+ } else {
124
+ mergeMissing(newEntry, oldEntry);
125
+ }
126
+ }
127
+ delete entries[LEGACY_CHANNEL_ID];
128
+ changes.push(
129
+ `Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`,
130
+ );
131
+ }
132
+ }
133
+ }
134
+
135
+ // 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
136
+ const tools = next.tools;
137
+ if (isRecord(tools)) {
138
+ for (const key of ["allow", "alsoAllow"] as const) {
139
+ const list = tools[key];
140
+ if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
141
+ tools[key] = replaceAndDedup(
142
+ list.filter((v): v is string => typeof v === "string"),
143
+ LEGACY_CHANNEL_ID,
144
+ TARGET_CHANNEL_ID,
145
+ );
146
+ changes.push(
147
+ `Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
148
+ );
149
+ }
150
+ }
151
+ }
152
+
153
+ return { config: next as OpenClawConfig, changes };
154
+ }
package/src/runtime.ts CHANGED
@@ -1927,7 +1927,13 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1927
1927
  : {}),
1928
1928
  },
1929
1929
  ...(memoryRoot ? { extra: { memoryRoot } } : {}),
1930
- ...(turn.peer.kind === "group"
1930
+ // Deliver the rendered ClawChat per-turn prompt (owner agent_behavior,
1931
+ // metadata, peer/sender profile) to the host for ALL chat types. The host
1932
+ // appends `GroupSystemPrompt` to the system prompt regardless of chat
1933
+ // kind. Direct chats previously relied only on the `before_prompt_build`
1934
+ // staging hook, which is not applied by the host for DM sessions, so the
1935
+ // owner-configured behavior never reached the LLM in 1:1 chats.
1936
+ ...(turnPrompt
1931
1937
  ? { supplemental: { groupSystemPrompt: turnPrompt } }
1932
1938
  : {}),
1933
1939
  }) as MutableOpenClawReplyContext;