@clawling/clawchat-plugin-openclaw 2026.9.8-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.
@@ -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.8-1",
3
+ "version": "2026.9.8-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -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,