@clawling/clawchat-plugin-openclaw 2026.9.13-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.
@@ -19,19 +19,20 @@ function applyPatch(target, patch) {
19
19
  return next;
20
20
  }
21
21
  /**
22
- * The RAW object a patch for `accountId` lands in (not the merged view — use
23
- * `readOpenclawClawlingAccountSection` for that). Default: the `accounts.default`
24
- * entry when the host's single-account promotion created one, else the root.
25
- * Named: the matching `accounts.<key>` entry (case-insensitive) or `{}`.
22
+ * Dotted config path of the slot {@link patchOpenclawClawlingAccountSection}
23
+ * writes `accountId` into, for operator-facing logs. Default:
24
+ * `channels.<channel>` — or `channels.<channel>.accounts.default` once the
25
+ * host's single-account promotion created that entry. Named:
26
+ * `channels.<channel>.accounts.<key>` with the existing key spelling.
26
27
  */
27
- export function readOpenclawClawlingAccountEntry(cfg, accountId) {
28
+ export function openclawClawlingAccountSlotPath(cfg, accountId) {
28
29
  const id = normalizeOpenclawClawlingAccountId(accountId);
29
30
  const section = readRecord(readRecord(cfg.channels)[CHANNEL_ID]);
30
- const accounts = readRecord(section.accounts);
31
- const key = findAccountKey(accounts, id);
31
+ const key = findAccountKey(readRecord(section.accounts), id);
32
+ const base = `channels.${CHANNEL_ID}`;
32
33
  if (key !== undefined)
33
- return readRecord(accounts[key]);
34
- return id === DEFAULT_ACCOUNT_ID ? section : {};
34
+ return `${base}.accounts.${key}`;
35
+ return id === DEFAULT_ACCOUNT_ID ? base : `${base}.accounts.${id}`;
35
36
  }
36
37
  /**
37
38
  * The single config write point. Pure. Default → root section (or
@@ -1,21 +1,44 @@
1
+ import fs from "node:fs";
1
2
  import { normalizeOpenclawClawlingAccountId } from "./config.js";
3
+ import { livewareNamedAccountRootDir } from "./liveware-cli.js";
2
4
  /**
3
5
  * `openclaw channels remove --account <id> [--delete]` — the host has already
4
6
  * rewritten the config (hybrid adapter) and stopped the account. Blank that
5
7
  * account's SQLite credentials so a later start of the same id cannot resume
6
8
  * the old identity from the row. Identity columns are kept (re-pair mode).
9
+ *
10
+ * A NAMED account also owns a private liveware root
11
+ * (`<stateDir>/clawchat/accounts/<id>/`: the CLI home holding the
12
+ * `$HOME/.clawling` login session, and the sample tree); the whole root is
13
+ * removed so the identity does not linger on disk. The `default` account's
14
+ * paths are the shared pre-multi-account layout and are never touched here.
15
+ * The host stops the account before this hook (best effort), so a still-open
16
+ * file only costs a log line.
7
17
  */
8
18
  export function clearOpenclawClawlingAccountOnRemoval(params) {
9
19
  const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
10
20
  if (!params.store) {
11
21
  params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite unavailable, nothing to clear`);
12
- return;
13
22
  }
23
+ else {
24
+ try {
25
+ params.store.clearActivationCredentials({ platform: "openclaw", accountId });
26
+ params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
27
+ }
28
+ catch (err) {
29
+ params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`);
30
+ }
31
+ }
32
+ if (!params.stateDir)
33
+ return;
34
+ const dir = livewareNamedAccountRootDir(params.stateDir, accountId);
35
+ if (dir === null)
36
+ return;
37
+ const rm = params.rmFn ?? ((target) => fs.rmSync(target, { recursive: true, force: true }));
14
38
  try {
15
- params.store.clearActivationCredentials({ platform: "openclaw", accountId });
16
- params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
39
+ rm(dir);
17
40
  }
18
41
  catch (err) {
19
- params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`);
42
+ params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; liveware directory ${dir} not removed: ${err instanceof Error ? err.message : String(err)}`);
20
43
  }
21
44
  }
@@ -28,8 +28,14 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
28
28
  lifecycle: {
29
29
  onAccountRemoved: ({ accountId, runtime }) => {
30
30
  let store = null;
31
+ let stateDir;
32
+ try {
33
+ stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
34
+ }
35
+ catch {
36
+ stateDir = undefined;
37
+ }
31
38
  try {
32
- const stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
33
39
  store = getClawChatStore({
34
40
  ...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
35
41
  log: { error: (message) => runtime.error(message) },
@@ -41,6 +47,7 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
41
47
  clearOpenclawClawlingAccountOnRemoval({
42
48
  accountId,
43
49
  store,
50
+ stateDir,
44
51
  log: (message) => runtime.log(message),
45
52
  });
46
53
  },
@@ -46,14 +46,23 @@ const configAdapter = {
46
46
  * `configured: true` again on the next gateway start. Clear the same keys
47
47
  * from the promoted entry so `--delete` actually deletes. Named ids are the
48
48
  * SDK's alone — it removes the whole `accounts.<id>` entry.
49
+ *
50
+ * Guard: a config with no channel section has nothing to clear, and the
51
+ * patch write would otherwise materialize an empty section out of nothing.
49
52
  */
50
53
  deleteAccount: (args) => {
51
54
  const next = hybridConfigAdapter.deleteAccount?.(args) ?? args.cfg;
52
55
  if (normalizeOpenclawClawlingAccountId(args.accountId) !== DEFAULT_ACCOUNT_ID)
53
56
  return next;
57
+ if (!hasOpenclawClawlingChannelSection(next))
58
+ return next;
54
59
  return patchOpenclawClawlingAccountSection(next, DEFAULT_ACCOUNT_ID, CLEAR_BASE_FIELDS_PATCH);
55
60
  },
56
61
  };
62
+ function hasOpenclawClawlingChannelSection(cfg) {
63
+ const section = cfg.channels?.[CHANNEL_ID];
64
+ return Boolean(section) && typeof section === "object" && !Array.isArray(section);
65
+ }
57
66
  function readInviteCode(input) {
58
67
  return typeof input.code === "string" && input.code.trim()
59
68
  ? input.code.trim()
@@ -230,7 +230,7 @@ export function readOpenclawClawlingAccountSection(cfg, accountId) {
230
230
  channelConfig: root,
231
231
  accountConfig: entry ?? {},
232
232
  omitKeys,
233
- nestedObjectKeys: NESTED_ACCOUNT_OBJECT_KEYS,
233
+ nestedObjectKeys: [...NESTED_ACCOUNT_OBJECT_KEYS],
234
234
  });
235
235
  }
236
236
  const accountListHelpers = createAccountListHelpers(CHANNEL_ID, {
@@ -26,6 +26,17 @@ function livewareAccountRoot(stateDir, accountId) {
26
26
  ? path.join(root, "clawchat")
27
27
  : path.join(root, "clawchat", "accounts", id);
28
28
  }
29
+ /**
30
+ * Root of a NAMED account's private liveware state
31
+ * (`<stateDir>/clawchat/accounts/<id>`), the directory account removal
32
+ * deletes wholesale. Returns null for `default`, whose root is the shared
33
+ * `<stateDir>/clawchat` and must never be removed this way.
34
+ */
35
+ export function livewareNamedAccountRootDir(stateDir, accountId) {
36
+ return normalizeOpenclawClawlingAccountId(accountId) === DEFAULT_ACCOUNT_ID
37
+ ? null
38
+ : livewareAccountRoot(stateDir, accountId);
39
+ }
29
40
  /**
30
41
  * Private HOME for the liveware CLI: `<stateDir>/clawchat/liveware-home` for
31
42
  * the default account, `<stateDir>/clawchat/accounts/<id>/liveware-home` for a
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline/promises";
2
2
  import { createOpenclawClawlingApiClient } from "./api-client.js";
3
3
  import { ClawlingApiError } from "./api-types.js";
4
4
  import { resolvePluginVersion } from "./plugin-report.js";
5
- import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
5
+ import { openclawClawlingAccountSlotPath, patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
6
6
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
7
7
  import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, } from "./config.js";
8
8
  import { getClawChatStore } from "./storage.js";
@@ -310,9 +310,7 @@ export async function runOpenclawClawlingLogin(params) {
310
310
  }
311
311
  : {}),
312
312
  };
313
- const configPath = accountId === DEFAULT_ACCOUNT_ID
314
- ? `channels.${CHANNEL_ID}`
315
- : `channels.${CHANNEL_ID}.accounts.${accountId}`;
313
+ const configPath = openclawClawlingAccountSlotPath(params.cfg, accountId);
316
314
  runtime.log(`Updating config: ${configPath}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
317
315
  await persistLoginConfig(accountId, params, normalizedResult);
318
316
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.9.13-1",
3
+ "version": "2026.9.13-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -22,21 +22,19 @@ function applyPatch(target: Record<string, unknown>, patch: Record<string, unkno
22
22
  }
23
23
 
24
24
  /**
25
- * The RAW object a patch for `accountId` lands in (not the merged view — use
26
- * `readOpenclawClawlingAccountSection` for that). Default: the `accounts.default`
27
- * entry when the host's single-account promotion created one, else the root.
28
- * Named: the matching `accounts.<key>` entry (case-insensitive) or `{}`.
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.
29
30
  */
30
- export function readOpenclawClawlingAccountEntry(
31
- cfg: OpenClawConfig,
32
- accountId?: string | null,
33
- ): Record<string, unknown> {
31
+ export function openclawClawlingAccountSlotPath(cfg: OpenClawConfig, accountId?: string | null): string {
34
32
  const id = normalizeOpenclawClawlingAccountId(accountId);
35
33
  const section = readRecord(readRecord((cfg as { channels?: unknown }).channels)[CHANNEL_ID]);
36
- const accounts = readRecord(section.accounts);
37
- const key = findAccountKey(accounts, id);
38
- if (key !== undefined) return readRecord(accounts[key]);
39
- return id === DEFAULT_ACCOUNT_ID ? section : {};
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}`;
40
38
  }
41
39
 
42
40
  /**
@@ -1,4 +1,6 @@
1
+ import fs from "node:fs";
1
2
  import { normalizeOpenclawClawlingAccountId } from "./config.ts";
3
+ import { livewareNamedAccountRootDir } from "./liveware-cli.ts";
2
4
  import type { ClawChatStore } from "./storage.ts";
3
5
 
4
6
  /**
@@ -6,23 +8,45 @@ import type { ClawChatStore } from "./storage.ts";
6
8
  * rewritten the config (hybrid adapter) and stopped the account. Blank that
7
9
  * account's SQLite credentials so a later start of the same id cannot resume
8
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.
9
19
  */
10
20
  export function clearOpenclawClawlingAccountOnRemoval(params: {
11
21
  accountId: string;
12
22
  store: Pick<ClawChatStore, "clearActivationCredentials"> | null;
23
+ /** Resolved OpenClaw state dir; absent → liveware directories are left alone. */
24
+ stateDir?: string | null;
13
25
  log?: (message: string) => void;
26
+ rmFn?: (dir: string) => void;
14
27
  }): void {
15
28
  const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
16
29
  if (!params.store) {
17
30
  params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite unavailable, nothing to clear`);
18
- return;
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
+ }
19
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 }));
20
45
  try {
21
- params.store.clearActivationCredentials({ platform: "openclaw", accountId });
22
- params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
46
+ rm(dir);
23
47
  } catch (err) {
24
48
  params.log?.(
25
- `[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`,
49
+ `[${accountId}] clawchat-plugin-openclaw account removed; liveware directory ${dir} not removed: ${err instanceof Error ? err.message : String(err)}`,
26
50
  );
27
51
  }
28
52
  }
@@ -67,14 +67,23 @@ const configAdapter: typeof hybridConfigAdapter = {
67
67
  * `configured: true` again on the next gateway start. Clear the same keys
68
68
  * from the promoted entry so `--delete` actually deletes. Named ids are the
69
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.
70
73
  */
71
74
  deleteAccount: (args) => {
72
75
  const next = hybridConfigAdapter.deleteAccount?.(args) ?? args.cfg;
73
76
  if (normalizeOpenclawClawlingAccountId(args.accountId) !== DEFAULT_ACCOUNT_ID) return next;
77
+ if (!hasOpenclawClawlingChannelSection(next)) return next;
74
78
  return patchOpenclawClawlingAccountSection(next, DEFAULT_ACCOUNT_ID, CLEAR_BASE_FIELDS_PATCH);
75
79
  },
76
80
  };
77
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
+
78
87
  function readInviteCode(input: ChannelSetupInput): string {
79
88
  return typeof input.code === "string" && input.code.trim()
80
89
  ? input.code.trim()
package/src/channel.ts CHANGED
@@ -38,8 +38,13 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
38
38
  lifecycle: {
39
39
  onAccountRemoved: ({ accountId, runtime }) => {
40
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
+ }
41
47
  try {
42
- const stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
43
48
  store = getClawChatStore({
44
49
  ...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
45
50
  log: { error: (message: string) => runtime.error(message) },
@@ -50,6 +55,7 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
50
55
  clearOpenclawClawlingAccountOnRemoval({
51
56
  accountId,
52
57
  store,
58
+ stateDir,
53
59
  log: (message) => runtime.log(message),
54
60
  });
55
61
  },
package/src/config.ts CHANGED
@@ -342,7 +342,7 @@ function readChannelSection(cfg: OpenClawConfig): Record<string, unknown> {
342
342
  /** Keys that identify a ClawChat account. Never inherited by a named account from the root. */
343
343
  export const ACCOUNT_CREDENTIAL_KEYS = ["token", "refreshToken", "agentId", "userId", "ownerUserId"] as const;
344
344
  const ACCOUNT_MAP_KEYS = ["accounts", "defaultAccount"] as const;
345
- const NESTED_ACCOUNT_OBJECT_KEYS = ["reconnect", "heartbeat", "ack"];
345
+ const NESTED_ACCOUNT_OBJECT_KEYS = ["reconnect", "heartbeat", "ack"] as const;
346
346
 
347
347
  /** Host-identical account id normalization (lowercase, `[^a-z0-9_-]` → `-`, blank → `default`). */
348
348
  export function normalizeOpenclawClawlingAccountId(value?: string | null): string {
@@ -381,7 +381,7 @@ export function readOpenclawClawlingAccountSection(
381
381
  channelConfig: root,
382
382
  accountConfig: entry ?? {},
383
383
  omitKeys,
384
- nestedObjectKeys: NESTED_ACCOUNT_OBJECT_KEYS,
384
+ nestedObjectKeys: [...NESTED_ACCOUNT_OBJECT_KEYS],
385
385
  });
386
386
  }
387
387
 
@@ -37,6 +37,18 @@ function livewareAccountRoot(stateDir: string | undefined, accountId?: string |
37
37
  : path.join(root, "clawchat", "accounts", id);
38
38
  }
39
39
 
40
+ /**
41
+ * Root of a NAMED account's private liveware state
42
+ * (`<stateDir>/clawchat/accounts/<id>`), the directory account removal
43
+ * deletes wholesale. Returns null for `default`, whose root is the shared
44
+ * `<stateDir>/clawchat` and must never be removed this way.
45
+ */
46
+ export function livewareNamedAccountRootDir(stateDir: string, accountId?: string | null): string | null {
47
+ return normalizeOpenclawClawlingAccountId(accountId) === DEFAULT_ACCOUNT_ID
48
+ ? null
49
+ : livewareAccountRoot(stateDir, accountId);
50
+ }
51
+
40
52
  /**
41
53
  * Private HOME for the liveware CLI: `<stateDir>/clawchat/liveware-home` for
42
54
  * the default account, `<stateDir>/clawchat/accounts/<id>/liveware-home` for a
@@ -716,9 +716,10 @@ export type LivewareSampleSupervisorDeps = {
716
716
  ref?: string;
717
717
  spawnFn?: SpawnLike;
718
718
  execFileFn?: ExecFileLike;
719
- /** Private HOME for the liveware CLI (`livewareCliHomeDir(stateDir)`), so two
720
- * instances on one host do not share `$HOME/.clawling`. Absent → the CLI
721
- * inherits the ambient HOME, which is the pre-isolation behavior. */
719
+ /** Private HOME for the liveware CLI (`livewareCliHomeDir(stateDir, accountId)`),
720
+ * so two instances on one host — or two accounts in one gateway — do not
721
+ * share `$HOME/.clawling`. Absent → the CLI inherits the ambient HOME, which
722
+ * is the pre-isolation behavior. */
722
723
  cliHome?: string;
723
724
  probePortFn?: ProbePortLike;
724
725
  log?: LivewareLogger;
@@ -3,7 +3,7 @@ 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
5
  import { resolvePluginVersion } from "./plugin-report.ts";
6
- import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
6
+ import { openclawClawlingAccountSlotPath, patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
7
7
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
8
8
  import {
9
9
  CHANNEL_ID,
@@ -408,9 +408,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
408
408
  : {}),
409
409
  };
410
410
 
411
- const configPath = accountId === DEFAULT_ACCOUNT_ID
412
- ? `channels.${CHANNEL_ID}`
413
- : `channels.${CHANNEL_ID}.accounts.${accountId}`;
411
+ const configPath = openclawClawlingAccountSlotPath(params.cfg, accountId);
414
412
  runtime.log(
415
413
  `Updating config: ${configPath}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
416
414
  normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""