@clawling/clawchat-plugin-openclaw 2026.9.7-1 → 2026.9.8-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/README.md CHANGED
@@ -31,7 +31,7 @@ Install and activate the ClawChat plugin from npm package @clawling/clawchat-plu
31
31
  Pick one of these invite-code activation paths after the plugin is loaded
32
32
  into OpenClaw:
33
33
 
34
- - **Runtime slash command (recommended).** Send `/clawchat-activate A1B2C3`
34
+ - **Runtime slash command (recommended).** Send `/clawchat-activate K7RM4TQP`
35
35
  in the chat where OpenClaw is running. Not a shell command — running
36
36
  `openclaw clawchat-activate` is expected to fail. This is the reliable
37
37
  first-time activation path on OpenClaw 2026.5.5 and newer.
@@ -4,9 +4,27 @@ const ACTIVATION_FLAGS = ["--new-account", "--repair"];
4
4
  function stripActivationFlags(raw) {
5
5
  return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
6
6
  }
7
+ /**
8
+ * A whole argument token that looks like a ClawChat connect code.
9
+ *
10
+ * member-backend mints **8** characters drawn from
11
+ * `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, with 0/1/I/O excluded so humans can
12
+ * transcribe them. This deliberately accepts a band around that length rather
13
+ * than exactly 8: the length is the server's to choose, and pinning it here is
14
+ * what previously broke activation outright. A too-long or too-short token is left for the server to reject —
15
+ * the code is checked there anyway, so a permissive local filter costs nothing
16
+ * while a strict one silently swallows valid codes.
17
+ *
18
+ * Matching whole tokens (not a substring scan) is what keeps the command word
19
+ * itself out of the result when `ctx.args` is absent and we fall back to
20
+ * `ctx.commandBody`, which still carries `/clawchat-activate`.
21
+ */
22
+ const INVITE_CODE_PATTERN = /^[A-Z0-9]{6,12}$/u;
7
23
  function extractInviteCode(value) {
8
24
  const raw = typeof value === "string" ? value.trim() : "";
9
- return stripActivationFlags(raw).match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
25
+ return (stripActivationFlags(raw)
26
+ .split(/\s+/u)
27
+ .find((token) => INVITE_CODE_PATTERN.test(token)) ?? "");
10
28
  }
11
29
  /**
12
30
  * Read the activation intent from the command args.
@@ -102,14 +120,14 @@ function formatOutputVisibilityResult(outputVisibility) {
102
120
  export function registerOpenclawClawlingCommands(api) {
103
121
  api.registerCommand({
104
122
  name: "clawchat-activate",
105
- description: "Activate ClawChat with an invite code, e.g. /clawchat-activate A1B2C3.",
123
+ description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP.",
106
124
  acceptsArgs: true,
107
125
  requireAuth: true,
108
126
  async handler(ctx) {
109
127
  const rawArgs = ctx.args ?? ctx.commandBody;
110
128
  const code = extractInviteCode(rawArgs);
111
129
  if (!code) {
112
- return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
130
+ return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP" };
113
131
  }
114
132
  const intent = extractActivationIntent(rawArgs);
115
133
  try {
@@ -37,17 +37,17 @@ export class ExistingActivationError extends Error {
37
37
  agentId;
38
38
  constructor(userId, agentId = "") {
39
39
  const who = agentId ? `agent ${agentId} (shadow user ${userId})` : `agent ${userId}`;
40
- super(`this OpenClaw instance is already paired to ClawChat ${who}. ` +
41
- "Redeeming another invite code here would re-bind that code to the SAME " +
42
- "agent — no second agent is created, and this instance's credentials are " +
43
- "overwritten.\n" +
44
- " - To run a SECOND agent alongside this one, give it its own OpenClaw " +
45
- "home:\n" +
46
- " OPENCLAW_HOME=<path> openclaw channels login --channel clawchat-plugin-openclaw\n" +
47
- " - To REPLACE this instance's agent with a brand-new identity: " +
48
- "/clawchat-activate <CODE> --new-account\n" +
49
- " - To re-pair THIS agent (e.g. after losing its token): " +
50
- "/clawchat-activate <CODE> --repair");
40
+ super(`this OpenClaw instance is already paired to ClawChat ${who}, and there is ` +
41
+ "nobody here to ask which outcome you want. The invite code was NOT " +
42
+ "spent. Re-run stating the intent:\n" +
43
+ " - Pair as a BRAND-NEW agent (this instance stops using the identity " +
44
+ "above): /clawchat-activate <CODE> --new-account\n" +
45
+ " - RESTORE the identity above (re-pairs that same agent; if it was " +
46
+ "deleted this brings it back with its history): " +
47
+ "/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");
51
51
  this.name = "ExistingActivationError";
52
52
  this.userId = userId;
53
53
  this.agentId = agentId;
@@ -76,6 +76,43 @@ async function promptInviteCodeFromStdin(runtime) {
76
76
  rl?.close();
77
77
  }
78
78
  }
79
+ /**
80
+ * Ask the operator which outcome they want, on the same stdin channel the
81
+ * invite-code prompt already uses.
82
+ *
83
+ * Returns `null` when nobody can answer. Both outcomes are irreversible in
84
+ * opposite directions — "new" strands the agent this instance currently holds,
85
+ * "restore" can revive an agent the owner just deleted — so an unattended run
86
+ * must not pick one. `null` refuses instead, leaving the code redeemable.
87
+ *
88
+ * A non-TTY stdin is the unattended case: a piped/closed stdin makes
89
+ * `rl.question` resolve instantly with an empty line, which would otherwise
90
+ * read as a silent answer. An unrecognized reply is also `null`: this is the
91
+ * one prompt where guessing at the operator's meaning is worse than stopping.
92
+ */
93
+ async function promptActivationIntentFromStdin(runtime, who) {
94
+ if (!process.stdin.isTTY)
95
+ return null;
96
+ runtime.log(`This OpenClaw instance already holds ClawChat ${who}.\n` +
97
+ " [1] Pair as a BRAND-NEW agent — this instance stops using that identity.\n" +
98
+ " [2] RESTORE that identity — re-pairs the same agent; if it was deleted, " +
99
+ "this brings it back with its history.\n" +
100
+ "Enter 1 or 2 (press Enter to submit):");
101
+ let rl;
102
+ try {
103
+ rl = createInterface({ input: process.stdin, output: process.stdout });
104
+ const answer = (await rl.question("> ")).trim();
105
+ if (answer === "1")
106
+ return "new-account";
107
+ if (answer === "2")
108
+ return "repair";
109
+ runtime.log(`Unrecognized choice ${JSON.stringify(answer)} — not activating.`);
110
+ return null;
111
+ }
112
+ finally {
113
+ rl?.close();
114
+ }
115
+ }
79
116
  function buildLoginConfig(cfg, result) {
80
117
  const channels = (cfg.channels ?? {});
81
118
  const existing = (channels[CHANNEL_ID] ?? {});
@@ -165,13 +202,32 @@ export async function runOpenclawClawlingLogin(params) {
165
202
  // `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
166
203
  // overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
167
204
  const account = resolveOpenclawClawlingAccount(cfg);
168
- // Guard before the invite code is even read, so a refused activation never
169
- // spends the code — the operator can still redeem it the right way. "Live" is
170
- // decided by whether a usable token remains: auto-logout (§C) blanks the
171
- // tokens but preserves the identity, so a logged-out instance still re-pairs
172
- // with no flag at all, which is the flow the replay exists for.
173
- if (account.userId.trim() && account.token.trim() && !params.newAccount && !params.repair) {
174
- throw new ExistingActivationError(account.userId.trim(), account.agentId.trim());
205
+ // Fork before the invite code is even read, so neither branch spends the code
206
+ // before the outcome is settled. "Live" is decided by whether a usable token
207
+ // remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
208
+ // logged-out instance still re-pairs with no flag at all, which is the flow
209
+ // the replay exists for.
210
+ //
211
+ // Holding an identity is not itself an error — it just means the run is
212
+ // ambiguous, because redeeming a code here can either mint a new agent or
213
+ // re-pair the incumbent one. Ask which, and only refuse when nobody answers.
214
+ // An explicit `newAccount` / `repair` from the caller has already settled it,
215
+ // so no prompt.
216
+ let newAccount = params.newAccount;
217
+ if (account.userId.trim() && account.token.trim() && !newAccount && !params.repair) {
218
+ const identity = account.agentId.trim()
219
+ ? `agent ${account.agentId.trim()} (shadow user ${account.userId.trim()})`
220
+ : `agent ${account.userId.trim()}`;
221
+ const intent = await (params.readActivationIntent ??
222
+ (() => promptActivationIntentFromStdin(runtime, identity)))();
223
+ // Only "new-account" changes what gets sent. Restoring needs no flag:
224
+ // replaying the stored `user_id` IS the re-pair, and that is already the
225
+ // default below — so choosing it simply means "don't refuse".
226
+ if (intent === "new-account")
227
+ newAccount = true;
228
+ else if (intent === null) {
229
+ throw new ExistingActivationError(account.userId.trim(), account.agentId.trim());
230
+ }
175
231
  }
176
232
  const inviteCode = (await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()).trim();
177
233
  if (!inviteCode) {
@@ -189,7 +245,7 @@ export async function runOpenclawClawlingLogin(params) {
189
245
  let result;
190
246
  // A brand-new identity is precisely "do not replay" — the replay is the only
191
247
  // thing that would bind this code to the incumbent agent.
192
- const existingUserId = params.newAccount ? "" : account.userId.trim();
248
+ const existingUserId = newAccount ? "" : account.userId.trim();
193
249
  try {
194
250
  result = await apiClient.agentsConnect({
195
251
  code: inviteCode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.9.7-1",
3
+ "version": "2026.9.8-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
package/src/commands.ts CHANGED
@@ -10,9 +10,30 @@ function stripActivationFlags(raw: string): string {
10
10
  return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
11
11
  }
12
12
 
13
+ /**
14
+ * A whole argument token that looks like a ClawChat connect code.
15
+ *
16
+ * member-backend mints **8** characters drawn from
17
+ * `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, with 0/1/I/O excluded so humans can
18
+ * transcribe them. This deliberately accepts a band around that length rather
19
+ * than exactly 8: the length is the server's to choose, and pinning it here is
20
+ * what previously broke activation outright. A too-long or too-short token is left for the server to reject —
21
+ * the code is checked there anyway, so a permissive local filter costs nothing
22
+ * while a strict one silently swallows valid codes.
23
+ *
24
+ * Matching whole tokens (not a substring scan) is what keeps the command word
25
+ * itself out of the result when `ctx.args` is absent and we fall back to
26
+ * `ctx.commandBody`, which still carries `/clawchat-activate`.
27
+ */
28
+ const INVITE_CODE_PATTERN = /^[A-Z0-9]{6,12}$/u;
29
+
13
30
  function extractInviteCode(value: unknown): string {
14
31
  const raw = typeof value === "string" ? value.trim() : "";
15
- return stripActivationFlags(raw).match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
32
+ return (
33
+ stripActivationFlags(raw)
34
+ .split(/\s+/u)
35
+ .find((token) => INVITE_CODE_PATTERN.test(token)) ?? ""
36
+ );
16
37
  }
17
38
 
18
39
  /**
@@ -118,14 +139,14 @@ function formatOutputVisibilityResult(outputVisibility: OutputVisibility): strin
118
139
  export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
119
140
  api.registerCommand({
120
141
  name: "clawchat-activate",
121
- description: "Activate ClawChat with an invite code, e.g. /clawchat-activate A1B2C3.",
142
+ description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP.",
122
143
  acceptsArgs: true,
123
144
  requireAuth: true,
124
145
  async handler(ctx) {
125
146
  const rawArgs = ctx.args ?? ctx.commandBody;
126
147
  const code = extractInviteCode(rawArgs);
127
148
  if (!code) {
128
- return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
149
+ return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP" };
129
150
  }
130
151
  const intent = extractActivationIntent(rawArgs);
131
152
  try {
@@ -63,8 +63,20 @@ export interface LoginParams {
63
63
  newAccount?: boolean;
64
64
  /** Re-pair the agent this instance already holds (keeps its identity). */
65
65
  repair?: boolean;
66
+ /**
67
+ * Ask the operator which outcome they want when this instance already holds
68
+ * an identity and the caller stated no intent.
69
+ *
70
+ * Resolving `null` means "nobody could be asked" and activation is refused
71
+ * with `ExistingActivationError`. Defaults to
72
+ * `promptActivationIntentFromStdin`; overridden by tests.
73
+ */
74
+ readActivationIntent?: () => Promise<ActivationIntent | null>;
66
75
  }
67
76
 
77
+ /** The two outcomes available once an identity is already stored. */
78
+ export type ActivationIntent = "new-account" | "repair";
79
+
68
80
  /**
69
81
  * Thrown when redeeming an invite code would silently replace a live activation.
70
82
  *
@@ -83,17 +95,17 @@ export class ExistingActivationError extends Error {
83
95
  constructor(userId: string, agentId = "") {
84
96
  const who = agentId ? `agent ${agentId} (shadow user ${userId})` : `agent ${userId}`;
85
97
  super(
86
- `this OpenClaw instance is already paired to ClawChat ${who}. ` +
87
- "Redeeming another invite code here would re-bind that code to the SAME " +
88
- "agent — no second agent is created, and this instance's credentials are " +
89
- "overwritten.\n" +
90
- " - To run a SECOND agent alongside this one, give it its own OpenClaw " +
91
- "home:\n" +
92
- " OPENCLAW_HOME=<path> openclaw channels login --channel clawchat-plugin-openclaw\n" +
93
- " - To REPLACE this instance's agent with a brand-new identity: " +
94
- "/clawchat-activate <CODE> --new-account\n" +
95
- " - To re-pair THIS agent (e.g. after losing its token): " +
96
- "/clawchat-activate <CODE> --repair",
98
+ `this OpenClaw instance is already paired to ClawChat ${who}, and there is ` +
99
+ "nobody here to ask which outcome you want. The invite code was NOT " +
100
+ "spent. Re-run stating the intent:\n" +
101
+ " - Pair as a BRAND-NEW agent (this instance stops using the identity " +
102
+ "above): /clawchat-activate <CODE> --new-account\n" +
103
+ " - RESTORE the identity above (re-pairs that same agent; if it was " +
104
+ "deleted this brings it back with its history): " +
105
+ "/clawchat-activate <CODE> --repair\n" +
106
+ " - To run a SECOND agent alongside this one instead, give it its own " +
107
+ "OpenClaw home:\n" +
108
+ " OPENCLAW_HOME=<path> openclaw channels login --channel clawchat-plugin-openclaw",
97
109
  );
98
110
  this.name = "ExistingActivationError";
99
111
  this.userId = userId;
@@ -126,6 +138,45 @@ async function promptInviteCodeFromStdin(runtime: {
126
138
  }
127
139
  }
128
140
 
141
+ /**
142
+ * Ask the operator which outcome they want, on the same stdin channel the
143
+ * invite-code prompt already uses.
144
+ *
145
+ * Returns `null` when nobody can answer. Both outcomes are irreversible in
146
+ * opposite directions — "new" strands the agent this instance currently holds,
147
+ * "restore" can revive an agent the owner just deleted — so an unattended run
148
+ * must not pick one. `null` refuses instead, leaving the code redeemable.
149
+ *
150
+ * A non-TTY stdin is the unattended case: a piped/closed stdin makes
151
+ * `rl.question` resolve instantly with an empty line, which would otherwise
152
+ * read as a silent answer. An unrecognized reply is also `null`: this is the
153
+ * one prompt where guessing at the operator's meaning is worse than stopping.
154
+ */
155
+ async function promptActivationIntentFromStdin(
156
+ runtime: { log: (message: string) => void },
157
+ who: string,
158
+ ): Promise<ActivationIntent | null> {
159
+ if (!process.stdin.isTTY) return null;
160
+ runtime.log(
161
+ `This OpenClaw instance already holds ClawChat ${who}.\n` +
162
+ " [1] Pair as a BRAND-NEW agent — this instance stops using that identity.\n" +
163
+ " [2] RESTORE that identity — re-pairs the same agent; if it was deleted, " +
164
+ "this brings it back with its history.\n" +
165
+ "Enter 1 or 2 (press Enter to submit):",
166
+ );
167
+ let rl: ReadlineInterface | undefined;
168
+ try {
169
+ rl = createInterface({ input: process.stdin, output: process.stdout });
170
+ const answer = (await rl.question("> ")).trim();
171
+ if (answer === "1") return "new-account";
172
+ if (answer === "2") return "repair";
173
+ runtime.log(`Unrecognized choice ${JSON.stringify(answer)} — not activating.`);
174
+ return null;
175
+ } finally {
176
+ rl?.close();
177
+ }
178
+ }
179
+
129
180
  function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
130
181
  const channels = (cfg.channels ?? {}) as Record<string, unknown>;
131
182
  const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
@@ -235,13 +286,31 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
235
286
  // overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
236
287
  const account = resolveOpenclawClawlingAccount(cfg);
237
288
 
238
- // Guard before the invite code is even read, so a refused activation never
239
- // spends the code — the operator can still redeem it the right way. "Live" is
240
- // decided by whether a usable token remains: auto-logout (§C) blanks the
241
- // tokens but preserves the identity, so a logged-out instance still re-pairs
242
- // with no flag at all, which is the flow the replay exists for.
243
- if (account.userId.trim() && account.token.trim() && !params.newAccount && !params.repair) {
244
- throw new ExistingActivationError(account.userId.trim(), account.agentId.trim());
289
+ // Fork before the invite code is even read, so neither branch spends the code
290
+ // before the outcome is settled. "Live" is decided by whether a usable token
291
+ // remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
292
+ // logged-out instance still re-pairs with no flag at all, which is the flow
293
+ // the replay exists for.
294
+ //
295
+ // Holding an identity is not itself an error — it just means the run is
296
+ // ambiguous, because redeeming a code here can either mint a new agent or
297
+ // re-pair the incumbent one. Ask which, and only refuse when nobody answers.
298
+ // An explicit `newAccount` / `repair` from the caller has already settled it,
299
+ // so no prompt.
300
+ let newAccount = params.newAccount;
301
+ if (account.userId.trim() && account.token.trim() && !newAccount && !params.repair) {
302
+ const identity = account.agentId.trim()
303
+ ? `agent ${account.agentId.trim()} (shadow user ${account.userId.trim()})`
304
+ : `agent ${account.userId.trim()}`;
305
+ const intent = await (params.readActivationIntent ??
306
+ (() => promptActivationIntentFromStdin(runtime, identity)))();
307
+ // Only "new-account" changes what gets sent. Restoring needs no flag:
308
+ // replaying the stored `user_id` IS the re-pair, and that is already the
309
+ // default below — so choosing it simply means "don't refuse".
310
+ if (intent === "new-account") newAccount = true;
311
+ else if (intent === null) {
312
+ throw new ExistingActivationError(account.userId.trim(), account.agentId.trim());
313
+ }
245
314
  }
246
315
 
247
316
  const inviteCode = (
@@ -264,7 +333,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
264
333
  let result;
265
334
  // A brand-new identity is precisely "do not replay" — the replay is the only
266
335
  // thing that would bind this code to the incumbent agent.
267
- const existingUserId = params.newAccount ? "" : account.userId.trim();
336
+ const existingUserId = newAccount ? "" : account.userId.trim();
268
337
  try {
269
338
  result = await apiClient.agentsConnect({
270
339
  code: inviteCode,