@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.
@@ -11,6 +11,8 @@ import net from "node:net";
11
11
  import path from "node:path";
12
12
  import { DEFAULT_SKILLS_REF, OFFICIAL_SKILLS_BASE } from "./skill-update.js";
13
13
  import { livewareCliEnv } from "./liveware-cli.js";
14
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
15
+ import { normalizeOpenclawClawlingAccountId } from "./config.js";
14
16
  export const LIVEWARES_TARGET = "openclaw";
15
17
  export const LIVEWARE_SAMPLE_ID = "liveware-sample";
16
18
  export const LIVEWARE_SAMPLE_APP_NAME = "Liveware Sample";
@@ -127,6 +129,15 @@ async function localInstallMatches(appDir, entry) {
127
129
  return false; // missing / unreadable — treat as "not installed"
128
130
  }
129
131
  }
132
+ /**
133
+ * Suffix for the `app.tmp-…` / `app.old-…` staging dirs. The pid keeps two
134
+ * PROCESSES apart; the account id keeps two supervisors inside ONE process
135
+ * apart, which share a pid and could otherwise share a staging tree.
136
+ */
137
+ function stagingSuffix(accountId) {
138
+ const id = normalizeOpenclawClawlingAccountId(accountId);
139
+ return id === DEFAULT_ACCOUNT_ID ? `${process.pid}` : `${process.pid}-${id}`;
140
+ }
130
141
  /**
131
142
  * Download + verify + atomically install the sample under `<sampleRoot>/app`.
132
143
  * Preserves USER_DATA_FILES from an existing install. Throws on any failure;
@@ -155,7 +166,8 @@ export async function downloadLivewareSample(opts) {
155
166
  await fs.promises.rm(path.join(opts.sampleRoot, name), { recursive: true, force: true }).catch(() => { });
156
167
  }
157
168
  }
158
- const tmpDir = path.join(opts.sampleRoot, `app.tmp-${process.pid}`);
169
+ const swapSuffix = stagingSuffix(opts.accountId);
170
+ const tmpDir = path.join(opts.sampleRoot, `app.tmp-${swapSuffix}`);
159
171
  await fs.promises.rm(tmpDir, { recursive: true, force: true });
160
172
  await fs.promises.mkdir(tmpDir, { recursive: true });
161
173
  try {
@@ -182,7 +194,7 @@ export async function downloadLivewareSample(opts) {
182
194
  // handle, and doing it first leaves no app/ at all when it fails. Renaming
183
195
  // aside puts the new tree in place first and makes the delete best-effort:
184
196
  // a leftover app.old-<pid> is swept on the next run, never a broken install.
185
- const oldDir = path.join(opts.sampleRoot, `app.old-${process.pid}`);
197
+ const oldDir = path.join(opts.sampleRoot, `app.old-${swapSuffix}`);
186
198
  await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => { });
187
199
  if (fs.existsSync(appDir)) {
188
200
  await withFsSwapRetry(() => fs.promises.rename(appDir, oldDir));
@@ -737,6 +749,7 @@ export class LivewareSampleSupervisor {
737
749
  }
738
750
  const { version, appDir } = await downloadLivewareSample({
739
751
  fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
752
+ accountId: deps.accountId,
740
753
  });
741
754
  if (this.bailIfStopped())
742
755
  return;
@@ -826,6 +839,7 @@ export class LivewareSampleSupervisor {
826
839
  try {
827
840
  const dl = await downloadLivewareSample({
828
841
  fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
842
+ accountId: deps.accountId,
829
843
  });
830
844
  version = dl.version;
831
845
  appDir = dl.appDir;
@@ -2,7 +2,9 @@ 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 { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, resolveOpenclawClawlingAccount, } from "./config.js";
5
+ import { openclawClawlingAccountSlotPath, patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
6
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
7
+ import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, } from "./config.js";
6
8
  import { getClawChatStore } from "./storage.js";
7
9
  /**
8
10
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
@@ -24,8 +26,7 @@ export const AGENT_NOT_FOUND_CODE = 16001;
24
26
  /**
25
27
  * Thrown when redeeming an invite code would silently replace a live activation.
26
28
  *
27
- * This channel holds exactly one ClawChat identity — `channels.<id>` is a single
28
- * slot and `listOpenclawClawlingAccountIds()` returns a single account. Because
29
+ * Each account slot holds exactly one ClawChat identity. Because
29
30
  * the stored `user_id` is replayed as a re-pair hint, redeeming a *new* invite
30
31
  * code here cannot produce a second agent: the server binds the code to the
31
32
  * existing agent, consumes it, creates no second agent and no second contact
@@ -45,9 +46,9 @@ export class ExistingActivationError extends Error {
45
46
  " - RESTORE the identity above (re-pairs that same agent; if it was " +
46
47
  "deleted this brings it back with its history): " +
47
48
  "/clawchat-activate <CODE> --repair\n" +
48
- " - To run a SECOND agent alongside this one instead, give it its own " +
49
- "OpenClaw home:\n" +
50
- " OPENCLAW_HOME=<path> openclaw channels login --channel clawchat-plugin-openclaw");
49
+ " - To run a SECOND agent alongside this one instead, activate it as a " +
50
+ "named account: /clawchat-activate <CODE> --account <name> (or: openclaw " +
51
+ "channels add --channel clawchat-plugin-openclaw --account <name> --token <CODE>)");
51
52
  this.name = "ExistingActivationError";
52
53
  this.userId = userId;
53
54
  this.agentId = agentId;
@@ -113,37 +114,30 @@ async function promptActivationIntentFromStdin(runtime, who) {
113
114
  rl?.close();
114
115
  }
115
116
  }
116
- function buildLoginConfig(cfg, result) {
117
- const channels = (cfg.channels ?? {});
118
- const existing = (channels[CHANNEL_ID] ?? {});
117
+ function buildLoginConfig(cfg, accountId, result) {
118
+ const existing = readOpenclawClawlingAccountSection(cfg, accountId);
119
119
  const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
120
120
  ? existing.groupMode
121
121
  : "all";
122
122
  const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
123
123
  ? existing.groupCommandMode
124
124
  : "owner";
125
- const nextSection = {
126
- ...existing,
125
+ const patch = {
127
126
  enabled: true,
128
- groupMode,
129
- groupCommandMode,
127
+ // Group policy defaults are root-level shared keys; named accounts inherit
128
+ // them and only carry credentials.
129
+ ...(accountId === DEFAULT_ACCOUNT_ID ? { groupMode, groupCommandMode } : {}),
130
130
  token: result.access_token,
131
+ // `undefined` deletes the key so a login without a refresh token never
132
+ // leaves a stale one behind.
133
+ refreshToken: result.refresh_token ? result.refresh_token : undefined,
131
134
  ...(result.agent.id ? { agentId: result.agent.id } : {}),
132
135
  userId: result.agent.user_id,
133
136
  ownerUserId: result.agent.owner_id,
134
137
  };
135
- if (result.refresh_token) {
136
- nextSection.refreshToken = result.refresh_token;
137
- }
138
- else {
139
- delete nextSection.refreshToken;
140
- }
141
- return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow({
142
- ...cfg,
143
- channels: { ...channels, [CHANNEL_ID]: nextSection },
144
- }));
138
+ return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow(patchOpenclawClawlingAccountSection(cfg, accountId, patch)));
145
139
  }
146
- async function persistLoginConfig(params, result) {
140
+ async function persistLoginConfig(accountId, params, result) {
147
141
  if (params.mutateConfigFile) {
148
142
  params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`);
149
143
  await params.mutateConfigFile({
@@ -152,7 +146,7 @@ async function persistLoginConfig(params, result) {
152
146
  reason: "clawchat-plugin-openclaw credentials changed",
153
147
  },
154
148
  mutate(draft) {
155
- Object.assign(draft, buildLoginConfig(draft, result));
149
+ Object.assign(draft, buildLoginConfig(draft, accountId, result));
156
150
  },
157
151
  });
158
152
  params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
@@ -160,7 +154,7 @@ async function persistLoginConfig(params, result) {
160
154
  }
161
155
  if (params.persistConfig) {
162
156
  params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
163
- await params.persistConfig(buildLoginConfig(params.cfg, result));
157
+ await params.persistConfig(buildLoginConfig(params.cfg, accountId, result));
164
158
  params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
165
159
  return;
166
160
  }
@@ -198,10 +192,11 @@ function readOptionalConnectString(value, fieldName) {
198
192
  */
199
193
  export async function runOpenclawClawlingLogin(params) {
200
194
  const { cfg, runtime } = params;
195
+ const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
201
196
  // `resolveOpenclawClawlingAccount` falls back to the built-in
202
197
  // `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
203
198
  // overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
204
- const account = resolveOpenclawClawlingAccount(cfg);
199
+ const account = resolveOpenclawClawlingAccount(cfg, accountId);
205
200
  // Fork before the invite code is even read, so neither branch spends the code
206
201
  // before the outcome is settled. "Live" is decided by whether a usable token
207
202
  // remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
@@ -315,8 +310,9 @@ export async function runOpenclawClawlingLogin(params) {
315
310
  }
316
311
  : {}),
317
312
  };
318
- runtime.log(`Updating config: channels.${CHANNEL_ID}.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} …`);
319
- await persistLoginConfig(params, normalizedResult);
313
+ const configPath = openclawClawlingAccountSlotPath(params.cfg, accountId);
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} …`);
315
+ await persistLoginConfig(accountId, params, normalizedResult);
320
316
  try {
321
317
  const store = params.store ??
322
318
  getClawChatStore({
@@ -673,8 +673,8 @@ export const openclawClawlingOutbound = {
673
673
  textChunkLimit: 4000,
674
674
  ...createAttachedChannelResultAdapter({
675
675
  channel: CHANNEL_ID,
676
- sendText: async ({ cfg, to, text }) => {
677
- const account = resolveOpenclawClawlingAccount(cfg);
676
+ sendText: async ({ cfg, to, text, accountId }) => {
677
+ const account = resolveOpenclawClawlingAccount(cfg, accountId);
678
678
  const client = getOpenclawClawlingClient(account.accountId) ??
679
679
  (await waitForOpenclawClawlingClient(account.accountId));
680
680
  const target = parseOpenclawRecipient(to);
@@ -709,8 +709,8 @@ export const openclawClawlingOutbound = {
709
709
  messageId: result?.messageId ?? messageId,
710
710
  };
711
711
  },
712
- sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
713
- const account = resolveOpenclawClawlingAccount(cfg);
712
+ sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId }) => {
713
+ const account = resolveOpenclawClawlingAccount(cfg, accountId);
714
714
  const client = getOpenclawClawlingClient(account.accountId) ??
715
715
  (await waitForOpenclawClawlingClient(account.accountId));
716
716
  if (!mediaUrl?.trim()) {
@@ -6,13 +6,14 @@ import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from ".
6
6
  import { createOpenclawClawlingApiClient } from "./api-client.js";
7
7
  import { buildActivationBootstrapText } from "./activation-greeting.js";
8
8
  import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
9
- import path from "node:path";
10
- import { ensureLivewareCli, livewareCliHomeDir, resolveLivewarePath } from "./liveware-cli.js";
9
+ import { ensureLivewareCli, livewareCliHomeDir, livewareSampleRootDir, resolveLivewarePath, } from "./liveware-cli.js";
11
10
  import { LivewareSampleSupervisor, } from "./liveware-sample.js";
12
11
  import { ClawlingApiError } from "./api-types.js";
13
12
  import { RefreshManager } from "./refresh-manager.js";
14
13
  import { runOpenclawClawlingLogin, } from "./login.runtime.js";
15
- import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
14
+ import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
15
+ import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
16
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
16
17
  import { isValidChatId } from "./ws-client.js";
17
18
  import { dispatchOpenclawClawlingInbound } from "./inbound.js";
18
19
  import { PendingConsentStore, cleanupTombstonedManagedSkills, handleOwnerConsentReply, managedSkillExists, readLocalSkillState, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
@@ -130,22 +131,22 @@ export function classifyHelloFailReason(reason) {
130
131
  return "token-rejected";
131
132
  return "generic";
132
133
  }
133
- /** Read `channels.<CHANNEL_ID>.refreshToken` from a live config, or null. */
134
134
  /**
135
- * The refresh token paired with the configured access token. Channel config
136
- * `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` (declared in the manifest's
137
- * `channelEnvVars`) is honoured ONLY when the channel config carries no `token`
138
- * of its own — i.e. when the access token itself came from `CLAWCHAT_TOKEN` —
139
- * so an env-provisioned pair can rotate, while a login-written config token is
140
- * never paired with a stray refresh token left in the environment.
135
+ * The refresh token paired with the account's access token. The account
136
+ * section's `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` is process-wide, so
137
+ * it is honoured ONLY for the default account and ONLY when that section
138
+ * carries no `token` of its own (i.e. the access token itself came from
139
+ * `CLAWCHAT_TOKEN`) — an env-provisioned pair can rotate, while a login-written
140
+ * token is never paired with a stray refresh token left in the environment.
141
141
  */
142
- function readConfigRefreshToken(cfg, env = process.env) {
143
- const channels = cfg.channels;
144
- const channel = isRecord(channels) ? channels[CHANNEL_ID] : undefined;
145
- const refreshToken = isRecord(channel) ? channel.refreshToken : undefined;
142
+ function readConfigRefreshToken(cfg, accountId, env = process.env) {
143
+ const section = readOpenclawClawlingAccountSection(cfg, accountId);
144
+ const refreshToken = section.refreshToken;
146
145
  if (typeof refreshToken === "string" && refreshToken.trim())
147
146
  return refreshToken.trim();
148
- const configToken = isRecord(channel) ? channel.token : undefined;
147
+ if (normalizeOpenclawClawlingAccountId(accountId) !== DEFAULT_ACCOUNT_ID)
148
+ return null;
149
+ const configToken = section.token;
149
150
  if (typeof configToken === "string" && configToken.trim())
150
151
  return null;
151
152
  const envRefreshToken = env[CLAWCHAT_REFRESH_TOKEN_ENV];
@@ -457,6 +458,12 @@ export async function maybeAutoActivateFromEnv(params) {
457
458
  const code = (params.connectCode ?? "").trim();
458
459
  if (!code)
459
460
  return params.account;
461
+ // The env var is process-wide: it can only ever describe ONE identity, and
462
+ // that is the default account. A named account never redeems it.
463
+ if (normalizeOpenclawClawlingAccountId(params.account.accountId) !== DEFAULT_ACCOUNT_ID) {
464
+ params.log?.info?.(`[${params.account.accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but ignored: env activation is for the default account only`);
465
+ return params.account;
466
+ }
460
467
  if (accountHasConnectCredentials(params.account))
461
468
  return params.account;
462
469
  const accountId = params.account.accountId;
@@ -489,7 +496,7 @@ export async function maybeAutoActivateFromEnv(params) {
489
496
  params.log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} activation failed: ${err instanceof Error ? err.message : String(err)}`);
490
497
  return params.account;
491
498
  }
492
- const next = resolveAccount(params.cfg);
499
+ const next = resolveAccount(params.cfg, accountId);
493
500
  params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} activation succeeded hasToken=${Boolean(next.token)}`);
494
501
  return next;
495
502
  }
@@ -769,7 +776,7 @@ export async function startOpenclawClawlingGateway(params) {
769
776
  userId: account.userId,
770
777
  ownerUserId: account.ownerUserId,
771
778
  accessToken: account.token,
772
- refreshToken: readConfigRefreshToken(cfg),
779
+ refreshToken: readConfigRefreshToken(cfg, accountId),
773
780
  loginMethod: "config-seed",
774
781
  deviceId: CHANNEL_ID,
775
782
  });
@@ -851,13 +858,16 @@ export async function startOpenclawClawlingGateway(params) {
851
858
  // `RuntimeConnectionStore` only `Pick`s a subset of its methods, so widen
852
859
  // through the structural `LivewareSampleStoreLike` contract (Task 8/4).
853
860
  store: store,
854
- sampleRoot: path.join(stateDirForSample ?? "", "clawchat", "liveware-sample"),
861
+ // Per account: two supervisors in one process otherwise install, swap and
862
+ // delete inside the SAME `app/` tree. `default` keeps the legacy path.
863
+ sampleRoot: livewareSampleRootDir(stateDirForSample ?? "", accountId),
855
864
  resolveToken: () => account.token ?? "",
856
865
  resolveAgentUserId: () => account.userId ?? null,
857
866
  resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
858
- // Per-instance HOME for the liveware CLI: its credentials otherwise live
859
- // in the host-global `$HOME/.clawling`, which a second profile clobbers.
860
- cliHome: livewareCliHomeDir(stateDirForSample),
867
+ // Per-instance AND per-account HOME for the liveware CLI: its credentials
868
+ // otherwise live in the host-global `$HOME/.clawling`, which a second
869
+ // profile — or a second account in this very process — clobbers.
870
+ cliHome: livewareCliHomeDir(stateDirForSample, accountId),
861
871
  cliReady: livewareCliReady,
862
872
  listApps: async () => {
863
873
  const client = getConversationApiClient();
@@ -906,7 +916,7 @@ export async function startOpenclawClawlingGateway(params) {
906
916
  ? runtimeConfig.mutateConfigFile
907
917
  : undefined;
908
918
  };
909
- // §0/§C.1 — write the channel-config `token`/`refreshToken` keys. `tokens=null`
919
+ // §0/§C.1 — write the account slot's `token`/`refreshToken` keys. `tokens=null`
910
920
  // blanks them (auto-logout); otherwise persists the rotated pair.
911
921
  const persistConfigTokens = async (tokens) => {
912
922
  const mutateConfigFile = resolveMutateConfigFile();
@@ -917,17 +927,10 @@ export async function startOpenclawClawlingGateway(params) {
917
927
  await mutateConfigFile({
918
928
  afterWrite: { mode: "none", reason: "clawchat-plugin-openclaw token refresh" },
919
929
  mutate(draft) {
920
- const channels = (draft.channels ?? {});
921
- const existing = (channels[CHANNEL_ID] ?? {});
922
- const nextSection = {
923
- ...existing,
930
+ Object.assign(draft, patchOpenclawClawlingAccountSection(draft, accountId, {
924
931
  token: tokens ? tokens.accessToken : "",
925
932
  refreshToken: tokens ? tokens.refreshToken : "",
926
- };
927
- Object.assign(draft, {
928
- ...draft,
929
- channels: { ...channels, [CHANNEL_ID]: nextSection },
930
- });
933
+ }));
931
934
  },
932
935
  });
933
936
  };
@@ -1016,7 +1019,7 @@ export async function startOpenclawClawlingGateway(params) {
1016
1019
  (activationAccount.source === "sqlite" && store?.getActivationCredentials
1017
1020
  ? store.getActivationCredentials({ platform: "openclaw", accountId })?.refreshToken
1018
1021
  : null) ??
1019
- readConfigRefreshToken(cfg);
1022
+ readConfigRefreshToken(cfg, accountId);
1020
1023
  const refreshManager = new RefreshManager({
1021
1024
  baseUrl: account.baseUrl,
1022
1025
  deviceId: refreshDeviceId,