@clawling/clawchat-plugin-openclaw 2026.9.11-1 → 2026.9.13-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.
@@ -138,6 +138,81 @@
138
138
  "timeout": { "type": "integer", "minimum": 100 },
139
139
  "autoResendOnTimeout": { "type": "boolean" }
140
140
  }
141
+ },
142
+ "defaultAccount": { "type": "string" },
143
+ "accounts": {
144
+ "type": "object",
145
+ "additionalProperties": {
146
+ "type": "object",
147
+ "additionalProperties": false,
148
+ "properties": {
149
+ "enabled": { "type": "boolean" },
150
+ "websocketUrl": { "type": "string" },
151
+ "baseUrl": { "type": "string" },
152
+ "mediaBaseUrl": { "type": "string" },
153
+ "token": { "type": "string" },
154
+ "refreshToken": { "type": "string" },
155
+ "agentId": { "type": "string" },
156
+ "userId": { "type": "string" },
157
+ "ownerUserId": { "type": "string" },
158
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] },
159
+ "chats": {
160
+ "type": "object",
161
+ "additionalProperties": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "properties": {
165
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
166
+ }
167
+ }
168
+ },
169
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
170
+ "groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
171
+ "groups": {
172
+ "type": "object",
173
+ "additionalProperties": {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "properties": {
177
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
178
+ "groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
179
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
180
+ }
181
+ }
182
+ },
183
+ "forwardThinking": { "type": "boolean" },
184
+ "forwardToolCalls": { "type": "boolean" },
185
+ "richInteractions": { "type": "boolean" },
186
+ "awarenessNote": { "type": "boolean" },
187
+ "livewareSample": { "type": "boolean" },
188
+ "reconnect": {
189
+ "type": "object",
190
+ "additionalProperties": false,
191
+ "properties": {
192
+ "initialDelay": { "type": "integer", "minimum": 100 },
193
+ "maxDelay": { "type": "integer", "minimum": 100 },
194
+ "jitterRatio": { "type": "number", "minimum": 0 },
195
+ "maxRetries": { "type": "integer", "minimum": 0 }
196
+ }
197
+ },
198
+ "heartbeat": {
199
+ "type": "object",
200
+ "additionalProperties": false,
201
+ "properties": {
202
+ "interval": { "type": "integer", "minimum": 1000 },
203
+ "timeout": { "type": "integer", "minimum": 1000 }
204
+ }
205
+ },
206
+ "ack": {
207
+ "type": "object",
208
+ "additionalProperties": false,
209
+ "properties": {
210
+ "timeout": { "type": "integer", "minimum": 100 },
211
+ "autoResendOnTimeout": { "type": "boolean" }
212
+ }
213
+ }
214
+ }
215
+ }
141
216
  }
142
217
  }
143
218
  },
@@ -213,6 +288,81 @@
213
288
  "timeout": { "type": "integer", "minimum": 100 },
214
289
  "autoResendOnTimeout": { "type": "boolean" }
215
290
  }
291
+ },
292
+ "defaultAccount": { "type": "string" },
293
+ "accounts": {
294
+ "type": "object",
295
+ "additionalProperties": {
296
+ "type": "object",
297
+ "additionalProperties": false,
298
+ "properties": {
299
+ "enabled": { "type": "boolean" },
300
+ "websocketUrl": { "type": "string" },
301
+ "baseUrl": { "type": "string" },
302
+ "mediaBaseUrl": { "type": "string" },
303
+ "token": { "type": "string" },
304
+ "refreshToken": { "type": "string" },
305
+ "agentId": { "type": "string" },
306
+ "userId": { "type": "string" },
307
+ "ownerUserId": { "type": "string" },
308
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] },
309
+ "chats": {
310
+ "type": "object",
311
+ "additionalProperties": {
312
+ "type": "object",
313
+ "additionalProperties": false,
314
+ "properties": {
315
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
316
+ }
317
+ }
318
+ },
319
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
320
+ "groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
321
+ "groups": {
322
+ "type": "object",
323
+ "additionalProperties": {
324
+ "type": "object",
325
+ "additionalProperties": false,
326
+ "properties": {
327
+ "groupMode": { "type": "string", "enum": ["mention", "all"] },
328
+ "groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
329
+ "outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
330
+ }
331
+ }
332
+ },
333
+ "forwardThinking": { "type": "boolean" },
334
+ "forwardToolCalls": { "type": "boolean" },
335
+ "richInteractions": { "type": "boolean" },
336
+ "awarenessNote": { "type": "boolean" },
337
+ "livewareSample": { "type": "boolean" },
338
+ "reconnect": {
339
+ "type": "object",
340
+ "additionalProperties": false,
341
+ "properties": {
342
+ "initialDelay": { "type": "integer", "minimum": 100 },
343
+ "maxDelay": { "type": "integer", "minimum": 100 },
344
+ "jitterRatio": { "type": "number", "minimum": 0 },
345
+ "maxRetries": { "type": "integer", "minimum": 0 }
346
+ }
347
+ },
348
+ "heartbeat": {
349
+ "type": "object",
350
+ "additionalProperties": false,
351
+ "properties": {
352
+ "interval": { "type": "integer", "minimum": 1000 },
353
+ "timeout": { "type": "integer", "minimum": 1000 }
354
+ }
355
+ },
356
+ "ack": {
357
+ "type": "object",
358
+ "additionalProperties": false,
359
+ "properties": {
360
+ "timeout": { "type": "integer", "minimum": 100 },
361
+ "autoResendOnTimeout": { "type": "boolean" }
362
+ }
363
+ }
364
+ }
365
+ }
216
366
  }
217
367
  }
218
368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.9.11-1",
3
+ "version": "2026.9.13-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -0,0 +1,74 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
+ import { CHANNEL_ID, normalizeOpenclawClawlingAccountId } from "./config.ts";
4
+
5
+ function readRecord(value: unknown): Record<string, unknown> {
6
+ return value && typeof value === "object" && !Array.isArray(value)
7
+ ? (value as Record<string, unknown>)
8
+ : {};
9
+ }
10
+
11
+ function findAccountKey(accounts: Record<string, unknown>, accountId: string): string | undefined {
12
+ return Object.keys(accounts).find((key) => normalizeOpenclawClawlingAccountId(key) === accountId);
13
+ }
14
+
15
+ function applyPatch(target: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
16
+ const next: Record<string, unknown> = { ...target };
17
+ for (const [key, value] of Object.entries(patch)) {
18
+ if (value === undefined) delete next[key];
19
+ else next[key] = value;
20
+ }
21
+ return next;
22
+ }
23
+
24
+ /**
25
+ * Dotted config path of the slot {@link patchOpenclawClawlingAccountSection}
26
+ * writes `accountId` into, for operator-facing logs. Default:
27
+ * `channels.<channel>` — or `channels.<channel>.accounts.default` once the
28
+ * host's single-account promotion created that entry. Named:
29
+ * `channels.<channel>.accounts.<key>` with the existing key spelling.
30
+ */
31
+ export function openclawClawlingAccountSlotPath(cfg: OpenClawConfig, accountId?: string | null): string {
32
+ const id = normalizeOpenclawClawlingAccountId(accountId);
33
+ const section = readRecord(readRecord((cfg as { channels?: unknown }).channels)[CHANNEL_ID]);
34
+ const key = findAccountKey(readRecord(section.accounts), id);
35
+ const base = `channels.${CHANNEL_ID}`;
36
+ if (key !== undefined) return `${base}.accounts.${key}`;
37
+ return id === DEFAULT_ACCOUNT_ID ? base : `${base}.accounts.${id}`;
38
+ }
39
+
40
+ /**
41
+ * The single config write point. Pure. Default → root section (or
42
+ * `accounts.default` when that entry exists, so a host-promoted token is never
43
+ * shadowed by a stale copy); named → `accounts.<id>` keeping the existing key
44
+ * spelling. A patch value of `undefined` deletes the key. `enabled` is only
45
+ * touched when the patch carries it.
46
+ */
47
+ export function patchOpenclawClawlingAccountSection(
48
+ cfg: OpenClawConfig,
49
+ accountId: string | null | undefined,
50
+ patch: Record<string, unknown>,
51
+ ): OpenClawConfig {
52
+ const id = normalizeOpenclawClawlingAccountId(accountId);
53
+ const channels = readRecord((cfg as { channels?: unknown }).channels);
54
+ const section = readRecord(channels[CHANNEL_ID]);
55
+ const accounts = readRecord(section.accounts);
56
+ const existingKey = findAccountKey(accounts, id);
57
+ if (id === DEFAULT_ACCOUNT_ID && existingKey === undefined) {
58
+ return {
59
+ ...cfg,
60
+ channels: { ...channels, [CHANNEL_ID]: applyPatch(section, patch) },
61
+ } as OpenClawConfig;
62
+ }
63
+ const key = existingKey ?? id;
64
+ return {
65
+ ...cfg,
66
+ channels: {
67
+ ...channels,
68
+ [CHANNEL_ID]: {
69
+ ...section,
70
+ accounts: { ...accounts, [key]: applyPatch(readRecord(accounts[key]), patch) },
71
+ },
72
+ },
73
+ } as OpenClawConfig;
74
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs";
2
+ import { normalizeOpenclawClawlingAccountId } from "./config.ts";
3
+ import { livewareNamedAccountRootDir } from "./liveware-cli.ts";
4
+ import type { ClawChatStore } from "./storage.ts";
5
+
6
+ /**
7
+ * `openclaw channels remove --account <id> [--delete]` — the host has already
8
+ * rewritten the config (hybrid adapter) and stopped the account. Blank that
9
+ * account's SQLite credentials so a later start of the same id cannot resume
10
+ * the old identity from the row. Identity columns are kept (re-pair mode).
11
+ *
12
+ * A NAMED account also owns a private liveware root
13
+ * (`<stateDir>/clawchat/accounts/<id>/`: the CLI home holding the
14
+ * `$HOME/.clawling` login session, and the sample tree); the whole root is
15
+ * removed so the identity does not linger on disk. The `default` account's
16
+ * paths are the shared pre-multi-account layout and are never touched here.
17
+ * The host stops the account before this hook (best effort), so a still-open
18
+ * file only costs a log line.
19
+ */
20
+ export function clearOpenclawClawlingAccountOnRemoval(params: {
21
+ accountId: string;
22
+ store: Pick<ClawChatStore, "clearActivationCredentials"> | null;
23
+ /** Resolved OpenClaw state dir; absent → liveware directories are left alone. */
24
+ stateDir?: string | null;
25
+ log?: (message: string) => void;
26
+ rmFn?: (dir: string) => void;
27
+ }): void {
28
+ const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
29
+ if (!params.store) {
30
+ params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite unavailable, nothing to clear`);
31
+ } else {
32
+ try {
33
+ params.store.clearActivationCredentials({ platform: "openclaw", accountId });
34
+ params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
35
+ } catch (err) {
36
+ params.log?.(
37
+ `[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`,
38
+ );
39
+ }
40
+ }
41
+ if (!params.stateDir) return;
42
+ const dir = livewareNamedAccountRootDir(params.stateDir, accountId);
43
+ if (dir === null) return;
44
+ const rm = params.rmFn ?? ((target: string) => fs.rmSync(target, { recursive: true, force: true }));
45
+ try {
46
+ rm(dir);
47
+ } catch (err) {
48
+ params.log?.(
49
+ `[${accountId}] clawchat-plugin-openclaw account removed; liveware directory ${dir} not removed: ${err instanceof Error ? err.message : String(err)}`,
50
+ );
51
+ }
52
+ }
@@ -1,7 +1,8 @@
1
- import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
1
+ import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
2
  import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
3
3
  import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
4
4
  import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
5
+ import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
5
6
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
6
7
  import {
7
8
  createComputedAccountStatusAdapter,
@@ -11,79 +12,121 @@ import {
11
12
  CHANNEL_ID,
12
13
  canStartOpenclawClawlingAccount,
13
14
  listOpenclawClawlingAccountIds,
15
+ normalizeOpenclawClawlingAccountId,
14
16
  openclawClawlingConfigSchema,
15
17
  resolveOpenclawClawlingAccount,
18
+ resolveOpenclawClawlingDefaultAccountId,
16
19
  type ResolvedOpenclawClawlingAccount,
17
20
  } from "./config.ts";
18
21
  import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
19
22
 
20
- const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
23
+ /** Keys a `channels remove --account default --delete` clears from the section. */
24
+ const CLEAR_BASE_FIELDS = [
25
+ "websocketUrl",
26
+ "baseUrl",
27
+ "token",
28
+ "refreshToken",
29
+ "agentId",
30
+ "userId",
31
+ "ownerUserId",
32
+ "forwardThinking",
33
+ "forwardToolCalls",
34
+ "richInteractions",
35
+ "enabled",
36
+ ] as const;
37
+
38
+ /**
39
+ * Hybrid layout: the channel root IS the `default` account (and the
40
+ * inheritance base); named accounts live under `accounts.<id>`. The SDK
41
+ * adapter branches `setAccountEnabled` / `deleteAccount` on the id for us.
42
+ */
43
+ const hybridConfigAdapter = createHybridChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
21
44
  sectionKey: CHANNEL_ID,
22
- resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
23
- listAccountIds: () => listOpenclawClawlingAccountIds(),
24
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
25
- deleteMode: "clear-fields",
26
- clearBaseFields: [
27
- "websocketUrl",
28
- "baseUrl",
29
- "token",
30
- "userId",
31
- "forwardThinking",
32
- "forwardToolCalls",
33
- "richInteractions",
34
- "enabled",
35
- ],
45
+ resolveAccount: (cfg, accountId) => resolveOpenclawClawlingAccount(cfg, accountId),
46
+ listAccountIds: (cfg) => listOpenclawClawlingAccountIds(cfg),
47
+ defaultAccountId: (cfg) => resolveOpenclawClawlingDefaultAccountId(cfg),
48
+ preserveSectionOnDefaultDelete: true,
49
+ clearBaseFields: [...CLEAR_BASE_FIELDS],
36
50
  resolveAllowFrom: (account) => account.allowFrom,
37
51
  formatAllowFrom: () => [],
38
52
  });
39
53
 
54
+ /** `{ token: undefined, refreshToken: undefined, … }` — `undefined` deletes. */
55
+ const CLEAR_BASE_FIELDS_PATCH: Record<string, undefined> = Object.fromEntries(
56
+ CLEAR_BASE_FIELDS.map((key) => [key, undefined]),
57
+ );
58
+
59
+ const configAdapter: typeof hybridConfigAdapter = {
60
+ ...hybridConfigAdapter,
61
+ /**
62
+ * The SDK's default branch clears `clearBaseFields` from the ROOT only. Once
63
+ * the host's single-account promotion has moved the identity into
64
+ * `accounts.default`, that leaves the deleted default fully credentialed:
65
+ * the default reader merges the promoted entry back over the blanked root,
66
+ * `ownerUserId` returns via the token's `oid` claim, and the account resolves
67
+ * `configured: true` again on the next gateway start. Clear the same keys
68
+ * from the promoted entry so `--delete` actually deletes. Named ids are the
69
+ * SDK's alone — it removes the whole `accounts.<id>` entry.
70
+ *
71
+ * Guard: a config with no channel section has nothing to clear, and the
72
+ * patch write would otherwise materialize an empty section out of nothing.
73
+ */
74
+ deleteAccount: (args) => {
75
+ const next = hybridConfigAdapter.deleteAccount?.(args) ?? args.cfg;
76
+ if (normalizeOpenclawClawlingAccountId(args.accountId) !== DEFAULT_ACCOUNT_ID) return next;
77
+ if (!hasOpenclawClawlingChannelSection(next)) return next;
78
+ return patchOpenclawClawlingAccountSection(next, DEFAULT_ACCOUNT_ID, CLEAR_BASE_FIELDS_PATCH);
79
+ },
80
+ };
81
+
82
+ function hasOpenclawClawlingChannelSection(cfg: OpenClawConfig): boolean {
83
+ const section = (cfg as { channels?: Record<string, unknown> }).channels?.[CHANNEL_ID];
84
+ return Boolean(section) && typeof section === "object" && !Array.isArray(section);
85
+ }
86
+
87
+ function readInviteCode(input: ChannelSetupInput): string {
88
+ return typeof input.code === "string" && input.code.trim()
89
+ ? input.code.trim()
90
+ : typeof input.token === "string"
91
+ ? input.token.trim()
92
+ : "";
93
+ }
94
+
40
95
  /**
41
96
  * Invite-code setup adapter used by OpenClaw setup surfaces.
42
97
  *
43
- * `channels add --token` passes the invite code as setup input. The setup
44
- * write leaves channel config unchanged; `afterAccountConfigWritten` exchanges
45
- * the invite code and persists token/userId through the host runtime mutator.
98
+ * `channels add [--account <id>] --token <code>` passes the invite code as
99
+ * setup input and the (optional) account id from the host. The setup write
100
+ * leaves channel config unchanged; `afterAccountConfigWritten` exchanges the
101
+ * invite code and persists credentials for THAT account through the host
102
+ * runtime mutator.
46
103
  */
47
104
  const setupAdapter: NonNullable<ChannelPlugin<ResolvedOpenclawClawlingAccount>["setup"]> = {
48
- resolveAccountId: () => DEFAULT_ACCOUNT_ID,
49
- validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
50
- const inviteCode =
51
- typeof input.code === "string" && input.code.trim()
52
- ? input.code.trim()
53
- : typeof input.token === "string"
54
- ? input.token.trim()
55
- : "";
56
- if (!inviteCode) {
57
- return "ClawChat invite code is required.";
58
- }
59
- return null;
60
- },
105
+ resolveAccountId: ({ accountId }) => normalizeOpenclawClawlingAccountId(accountId),
106
+ validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) =>
107
+ readInviteCode(input) ? null : "ClawChat invite code is required.",
61
108
  applyAccountConfig: ({ cfg }: {
62
109
  cfg: OpenClawConfig;
63
110
  accountId: string;
64
111
  input: ChannelSetupInput;
65
112
  }) => cfg,
66
- afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
67
- runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten invoked");
68
- const code =
69
- typeof input.code === "string" && input.code.trim()
70
- ? input.code.trim()
71
- : typeof input.token === "string"
72
- ? input.token.trim()
73
- : "";
113
+ afterAccountConfigWritten: async ({ cfg, accountId, input, runtime }) => {
114
+ const id = normalizeOpenclawClawlingAccountId(accountId);
115
+ runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten invoked`);
116
+ const code = readInviteCode(input);
74
117
  if (!code) {
75
- runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten skipped: empty invite code");
118
+ runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten skipped: empty invite code`);
76
119
  return;
77
120
  }
78
121
  const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
79
122
  await runOpenclawClawlingLogin({
80
123
  cfg,
81
- accountId: null,
124
+ accountId: id,
82
125
  runtime: { log: (message: string) => runtime.log(message) },
83
126
  readInviteCode: async () => code,
84
127
  mutateConfigFile: mutateConfigFile as OpenclawClawchatMutateConfigFile,
85
128
  });
86
- runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten completed");
129
+ runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten completed`);
87
130
  },
88
131
  };
89
132
 
package/src/channel.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
3
+ import { clearOpenclawClawlingAccountOnRemoval } from "./account-removal.ts";
3
4
  import {
4
5
  CHANNEL_ID,
5
6
  canStartOpenclawClawlingAccount,
@@ -9,6 +10,7 @@ import {
9
10
  import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
10
11
  import { openclawClawlingOutbound } from "./outbound.ts";
11
12
  import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.ts";
13
+ import { clawChatDbPathForStateDir, getClawChatStore } from "./storage.ts";
12
14
  import { openclawClawlingSetupPlugin } from "./channel.setup.ts";
13
15
  import { getClawChatPlatformPrompt } from "./plugin-prompts.ts";
14
16
 
@@ -33,9 +35,34 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
33
35
  });
34
36
  },
35
37
  },
38
+ lifecycle: {
39
+ onAccountRemoved: ({ accountId, runtime }) => {
40
+ let store: ReturnType<typeof getClawChatStore> | null = null;
41
+ let stateDir: string | undefined;
42
+ try {
43
+ stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
44
+ } catch {
45
+ stateDir = undefined;
46
+ }
47
+ try {
48
+ store = getClawChatStore({
49
+ ...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
50
+ log: { error: (message: string) => runtime.error(message) },
51
+ });
52
+ } catch {
53
+ store = null;
54
+ }
55
+ clearOpenclawClawlingAccountOnRemoval({
56
+ accountId,
57
+ store,
58
+ stateDir,
59
+ log: (message) => runtime.log(message),
60
+ });
61
+ },
62
+ },
36
63
  gateway: {
37
64
  startAccount: async (ctx) => {
38
- const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
65
+ const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg, ctx.accountId);
39
66
  ctx.log?.info?.(
40
67
  `[${account.accountId}] clawchat-plugin-openclaw lifecycle START_ACCOUNT_CALLED configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
41
68
  );
package/src/commands.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import type { OpenClawConfig, PluginCommandContext } from "openclaw/plugin-sdk/core";
3
- import { CHANNEL_ID, type OutputVisibility } from "./config.ts";
3
+ import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
4
+ import {
5
+ CHANNEL_ID,
6
+ normalizeOpenclawClawlingAccountId,
7
+ readOpenclawClawlingAccountSection,
8
+ type OutputVisibility,
9
+ } from "./config.ts";
4
10
  import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
5
11
 
6
12
  /** Activation intent flags — the two escape hatches out of the live-activation guard. */
@@ -10,6 +16,32 @@ function stripActivationFlags(raw: string): string {
10
16
  return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
11
17
  }
12
18
 
19
+ /** `--account <id>` / `--account=<id>`; the value is removed before the code scan so it can never be mistaken for a code. */
20
+ const ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account(?:=|\s+)(\S+)/u;
21
+ /** `--account` with nothing after it — including a dangling `--account=`, which
22
+ * is a typo rather than "use the default account". */
23
+ const BARE_ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account=?(?=\s|$)/u;
24
+
25
+ function extractAccountFlag(value: unknown): { accountId: string | null; rest: string; error: string | null } {
26
+ const raw = typeof value === "string" ? value : "";
27
+ const match = ACCOUNT_FLAG_PATTERN.exec(raw);
28
+ if (match) {
29
+ const id = match[1] ?? "";
30
+ if (!id || id.startsWith("--")) {
31
+ return { accountId: null, rest: raw, error: "--account requires an account id" };
32
+ }
33
+ return {
34
+ accountId: normalizeOpenclawClawlingAccountId(id),
35
+ rest: raw.replace(match[0], " "),
36
+ error: null,
37
+ };
38
+ }
39
+ if (BARE_ACCOUNT_FLAG_PATTERN.test(raw)) {
40
+ return { accountId: null, rest: raw, error: "--account requires an account id" };
41
+ }
42
+ return { accountId: null, rest: raw, error: null };
43
+ }
44
+
13
45
  /**
14
46
  * A whole argument token that looks like a ClawChat connect code.
15
47
  *
@@ -95,29 +127,25 @@ function resolveCommandChatId(ctx: PluginCommandContext): string {
95
127
 
96
128
  function persistOutputVisibility(
97
129
  draft: OpenClawConfig,
130
+ accountId: string,
98
131
  chatId: string,
99
132
  outputVisibility: OutputVisibility,
100
133
  ): void {
101
- const channels = readRecord((draft as { channels?: unknown }).channels);
102
- const channel = readRecord(channels[CHANNEL_ID]);
103
- const chats = readRecord(channel.chats);
134
+ // Seeded from the MERGED view, never the raw target entry: `mergeAccountConfig`
135
+ // REPLACES `chats` rather than deep-merging it, so a first write seeded from an
136
+ // entry that has no `chats` of its own (a named account, or a `default` the
137
+ // host has promoted into `accounts.default`) would shadow every inherited
138
+ // conversation. Copying the inherited map forward and overriding one key keeps
139
+ // the resolved view additive.
140
+ const merged = readOpenclawClawlingAccountSection(draft, accountId);
141
+ const chats = readRecord(merged.chats);
104
142
  const chat = readRecord(chats[chatId]);
105
- Object.assign(draft, {
106
- ...draft,
107
- channels: {
108
- ...channels,
109
- [CHANNEL_ID]: {
110
- ...channel,
111
- chats: {
112
- ...chats,
113
- [chatId]: {
114
- ...chat,
115
- outputVisibility,
116
- },
117
- },
118
- },
119
- },
120
- });
143
+ Object.assign(
144
+ draft,
145
+ patchOpenclawClawlingAccountSection(draft, accountId, {
146
+ chats: { ...chats, [chatId]: { ...chat, outputVisibility } },
147
+ }),
148
+ );
121
149
  }
122
150
 
123
151
  function formatOutputVisibilityResult(outputVisibility: OutputVisibility): string {
@@ -139,21 +167,26 @@ function formatOutputVisibilityResult(outputVisibility: OutputVisibility): strin
139
167
  export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
140
168
  api.registerCommand({
141
169
  name: "clawchat-activate",
142
- description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP.",
170
+ description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP [--account <id>].",
143
171
  acceptsArgs: true,
144
172
  requireAuth: true,
145
173
  async handler(ctx) {
146
174
  const rawArgs = ctx.args ?? ctx.commandBody;
147
- const code = extractInviteCode(rawArgs);
175
+ const accountFlag = extractAccountFlag(rawArgs);
176
+ if (accountFlag.error) {
177
+ return { text: `${accountFlag.error}. Usage: /clawchat-activate K7RM4TQP [--account <id>]` };
178
+ }
179
+ const code = extractInviteCode(accountFlag.rest);
148
180
  if (!code) {
149
- return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP" };
181
+ return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP [--account <id>]" };
150
182
  }
151
- const intent = extractActivationIntent(rawArgs);
183
+ const intent = extractActivationIntent(accountFlag.rest);
184
+ const accountId = accountFlag.accountId ?? normalizeOpenclawClawlingAccountId(ctx.accountId);
152
185
  try {
153
186
  const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
154
187
  await runOpenclawClawlingLogin({
155
188
  cfg: ctx.config,
156
- accountId: ctx.accountId ?? null,
189
+ accountId,
157
190
  runtime: { log: (message: string) => api.logger?.info?.(message) },
158
191
  readInviteCode: async () => code,
159
192
  mutateConfigFile: (api.runtime.config as unknown as {
@@ -189,10 +222,11 @@ export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "r
189
222
  if (!mutateConfigFile) {
190
223
  return { text: "OpenClaw runtime config mutation is unavailable for /clawchat-output." };
191
224
  }
225
+ const accountId = normalizeOpenclawClawlingAccountId(ctx.accountId);
192
226
  await mutateConfigFile({
193
227
  afterWrite: { mode: "auto" },
194
228
  mutate(draft) {
195
- persistOutputVisibility(draft, chatId, outputVisibility);
229
+ persistOutputVisibility(draft, accountId, chatId, outputVisibility);
196
230
  },
197
231
  });
198
232
  return { text: formatOutputVisibilityResult(outputVisibility) };