@clawling/clawchat-plugin-openclaw 2026.7.25-1 → 2026.7.27-1
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/dist/src/commands.js +27 -2
- package/dist/src/login.runtime.js +80 -3
- package/package.json +1 -1
- package/src/commands.ts +30 -2
- package/src/login.runtime.ts +91 -3
package/dist/src/commands.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { CHANNEL_ID } from "./config.js";
|
|
2
|
+
/** Activation intent flags — the two escape hatches out of the live-activation guard. */
|
|
3
|
+
const ACTIVATION_FLAGS = ["--new-account", "--repair"];
|
|
4
|
+
function stripActivationFlags(raw) {
|
|
5
|
+
return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
|
|
6
|
+
}
|
|
2
7
|
function extractInviteCode(value) {
|
|
3
8
|
const raw = typeof value === "string" ? value.trim() : "";
|
|
4
|
-
return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
9
|
+
return stripActivationFlags(raw).match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the activation intent from the command args.
|
|
13
|
+
*
|
|
14
|
+
* Redeeming a code while this instance holds a live activation is refused by
|
|
15
|
+
* `runOpenclawClawlingLogin` (it would re-bind the code to the incumbent agent
|
|
16
|
+
* rather than add a second one). `/clawchat-activate` is the only activation
|
|
17
|
+
* surface whose arguments this plugin parses, so it carries the two intents:
|
|
18
|
+
* `--new-account` replaces the identity, `--repair` re-pairs the current one.
|
|
19
|
+
*/
|
|
20
|
+
function extractActivationIntent(value) {
|
|
21
|
+
const raw = typeof value === "string" ? value : "";
|
|
22
|
+
return {
|
|
23
|
+
newAccount: raw.includes("--new-account"),
|
|
24
|
+
repair: raw.includes("--repair"),
|
|
25
|
+
};
|
|
5
26
|
}
|
|
6
27
|
function errorMessage(err) {
|
|
7
28
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -85,10 +106,12 @@ export function registerOpenclawClawlingCommands(api) {
|
|
|
85
106
|
acceptsArgs: true,
|
|
86
107
|
requireAuth: true,
|
|
87
108
|
async handler(ctx) {
|
|
88
|
-
const
|
|
109
|
+
const rawArgs = ctx.args ?? ctx.commandBody;
|
|
110
|
+
const code = extractInviteCode(rawArgs);
|
|
89
111
|
if (!code) {
|
|
90
112
|
return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
|
|
91
113
|
}
|
|
114
|
+
const intent = extractActivationIntent(rawArgs);
|
|
92
115
|
try {
|
|
93
116
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
94
117
|
await runOpenclawClawlingLogin({
|
|
@@ -97,6 +120,8 @@ export function registerOpenclawClawlingCommands(api) {
|
|
|
97
120
|
runtime: { log: (message) => api.logger?.info?.(message) },
|
|
98
121
|
readInviteCode: async () => code,
|
|
99
122
|
mutateConfigFile: api.runtime.config.mutateConfigFile,
|
|
123
|
+
newAccount: intent.newAccount,
|
|
124
|
+
repair: intent.repair,
|
|
100
125
|
});
|
|
101
126
|
return { text: "✅ ClawChat activated successfully." };
|
|
102
127
|
}
|
|
@@ -14,6 +14,45 @@ export const AGENTS_CONNECT_PLATFORM = "openclaw";
|
|
|
14
14
|
* always a bot; humans don't log in through this flow.
|
|
15
15
|
*/
|
|
16
16
|
export const AGENTS_CONNECT_TYPE = "clawbot";
|
|
17
|
+
/**
|
|
18
|
+
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
19
|
+
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
20
|
+
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
21
|
+
* retries once.
|
|
22
|
+
*/
|
|
23
|
+
export const AGENT_NOT_FOUND_CODE = 16001;
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when redeeming an invite code would silently replace a live activation.
|
|
26
|
+
*
|
|
27
|
+
* This channel holds exactly one ClawChat identity — `channels.<id>` is a single
|
|
28
|
+
* slot and `listOpenclawClawlingAccountIds()` returns a single account. Because
|
|
29
|
+
* the stored `user_id` is replayed as a re-pair hint, redeeming a *new* invite
|
|
30
|
+
* code here cannot produce a second agent: the server binds the code to the
|
|
31
|
+
* existing agent, consumes it, creates no second agent and no second contact
|
|
32
|
+
* entry, and the local credentials are overwritten in place — losing the first
|
|
33
|
+
* agent with no error and no warning. Refuse instead, and name the real intents.
|
|
34
|
+
*/
|
|
35
|
+
export class ExistingActivationError extends Error {
|
|
36
|
+
userId;
|
|
37
|
+
agentId;
|
|
38
|
+
constructor(userId, agentId = "") {
|
|
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");
|
|
51
|
+
this.name = "ExistingActivationError";
|
|
52
|
+
this.userId = userId;
|
|
53
|
+
this.agentId = agentId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
17
56
|
/**
|
|
18
57
|
* Prompt the operator for an invite code.
|
|
19
58
|
*
|
|
@@ -126,6 +165,14 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
126
165
|
// `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
|
|
127
166
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
128
167
|
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());
|
|
175
|
+
}
|
|
129
176
|
const inviteCode = (await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()).trim();
|
|
130
177
|
if (!inviteCode) {
|
|
131
178
|
throw new Error("Login aborted: invite code is required.");
|
|
@@ -140,8 +187,10 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
140
187
|
});
|
|
141
188
|
runtime.log("Verifying invite code …");
|
|
142
189
|
let result;
|
|
190
|
+
// A brand-new identity is precisely "do not replay" — the replay is the only
|
|
191
|
+
// thing that would bind this code to the incumbent agent.
|
|
192
|
+
const existingUserId = params.newAccount ? "" : account.userId.trim();
|
|
143
193
|
try {
|
|
144
|
-
const existingUserId = account.userId.trim();
|
|
145
194
|
result = await apiClient.agentsConnect({
|
|
146
195
|
code: inviteCode,
|
|
147
196
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
@@ -151,9 +200,37 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
151
200
|
}
|
|
152
201
|
catch (err) {
|
|
153
202
|
if (err instanceof ClawlingApiError) {
|
|
154
|
-
|
|
203
|
+
// The stored userId is a re-pair hint, not a credential — the invite
|
|
204
|
+
// code alone decides ownership. AGENT_NOT_FOUND means it names an agent
|
|
205
|
+
// this server has never had (owner account deleted, or the config was
|
|
206
|
+
// repointed at another deployment), so replaying it would fail
|
|
207
|
+
// identically for every future invite code. A 16001 leaves the code
|
|
208
|
+
// `pending` server-side, so shedding the id and retrying once cannot
|
|
209
|
+
// double-spend it. Nothing else is retried: an owner mismatch is a real
|
|
210
|
+
// rejection, and a request that sent no id has no stale state to shed.
|
|
211
|
+
if (existingUserId && err.meta?.code === AGENT_NOT_FOUND_CODE) {
|
|
212
|
+
runtime.log(`Stored userId ${existingUserId} no longer exists on this server — retrying as a fresh pairing …`);
|
|
213
|
+
try {
|
|
214
|
+
result = await apiClient.agentsConnect({
|
|
215
|
+
code: inviteCode,
|
|
216
|
+
platform: AGENTS_CONNECT_PLATFORM,
|
|
217
|
+
type: AGENTS_CONNECT_TYPE,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (retryErr) {
|
|
221
|
+
if (retryErr instanceof ClawlingApiError) {
|
|
222
|
+
throw new Error(`agents/connect failed (${retryErr.kind}): ${retryErr.message}`);
|
|
223
|
+
}
|
|
224
|
+
throw retryErr;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
throw new Error(`agents/connect failed (${err.kind}): ${err.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw err;
|
|
155
233
|
}
|
|
156
|
-
throw err;
|
|
157
234
|
}
|
|
158
235
|
const accessToken = requireConnectString(result?.access_token, "access_token");
|
|
159
236
|
const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -3,9 +3,33 @@ import type { OpenClawConfig, PluginCommandContext } from "openclaw/plugin-sdk/c
|
|
|
3
3
|
import { CHANNEL_ID, type OutputVisibility } from "./config.ts";
|
|
4
4
|
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
5
5
|
|
|
6
|
+
/** Activation intent flags — the two escape hatches out of the live-activation guard. */
|
|
7
|
+
const ACTIVATION_FLAGS = ["--new-account", "--repair"] as const;
|
|
8
|
+
|
|
9
|
+
function stripActivationFlags(raw: string): string {
|
|
10
|
+
return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
function extractInviteCode(value: unknown): string {
|
|
7
14
|
const raw = typeof value === "string" ? value.trim() : "";
|
|
8
|
-
return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
15
|
+
return stripActivationFlags(raw).match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the activation intent from the command args.
|
|
20
|
+
*
|
|
21
|
+
* Redeeming a code while this instance holds a live activation is refused by
|
|
22
|
+
* `runOpenclawClawlingLogin` (it would re-bind the code to the incumbent agent
|
|
23
|
+
* rather than add a second one). `/clawchat-activate` is the only activation
|
|
24
|
+
* surface whose arguments this plugin parses, so it carries the two intents:
|
|
25
|
+
* `--new-account` replaces the identity, `--repair` re-pairs the current one.
|
|
26
|
+
*/
|
|
27
|
+
function extractActivationIntent(value: unknown): { newAccount: boolean; repair: boolean } {
|
|
28
|
+
const raw = typeof value === "string" ? value : "";
|
|
29
|
+
return {
|
|
30
|
+
newAccount: raw.includes("--new-account"),
|
|
31
|
+
repair: raw.includes("--repair"),
|
|
32
|
+
};
|
|
9
33
|
}
|
|
10
34
|
|
|
11
35
|
function errorMessage(err: unknown): string {
|
|
@@ -98,10 +122,12 @@ export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "r
|
|
|
98
122
|
acceptsArgs: true,
|
|
99
123
|
requireAuth: true,
|
|
100
124
|
async handler(ctx) {
|
|
101
|
-
const
|
|
125
|
+
const rawArgs = ctx.args ?? ctx.commandBody;
|
|
126
|
+
const code = extractInviteCode(rawArgs);
|
|
102
127
|
if (!code) {
|
|
103
128
|
return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
|
|
104
129
|
}
|
|
130
|
+
const intent = extractActivationIntent(rawArgs);
|
|
105
131
|
try {
|
|
106
132
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
|
|
107
133
|
await runOpenclawClawlingLogin({
|
|
@@ -112,6 +138,8 @@ export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "r
|
|
|
112
138
|
mutateConfigFile: (api.runtime.config as unknown as {
|
|
113
139
|
mutateConfigFile: OpenclawClawchatMutateConfigFile;
|
|
114
140
|
}).mutateConfigFile,
|
|
141
|
+
newAccount: intent.newAccount,
|
|
142
|
+
repair: intent.repair,
|
|
115
143
|
});
|
|
116
144
|
return { text: "✅ ClawChat activated successfully." };
|
|
117
145
|
} catch (err) {
|
package/src/login.runtime.ts
CHANGED
|
@@ -21,6 +21,13 @@ export const AGENTS_CONNECT_PLATFORM = "openclaw" as const;
|
|
|
21
21
|
* always a bot; humans don't log in through this flow.
|
|
22
22
|
*/
|
|
23
23
|
export const AGENTS_CONNECT_TYPE = "clawbot" as const;
|
|
24
|
+
/**
|
|
25
|
+
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
26
|
+
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
27
|
+
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
28
|
+
* retries once.
|
|
29
|
+
*/
|
|
30
|
+
export const AGENT_NOT_FOUND_CODE = 16001 as const;
|
|
24
31
|
|
|
25
32
|
export type OpenclawClawchatMutateConfigFile = <T = void>(params: {
|
|
26
33
|
afterWrite: { mode: "auto" } | { mode: "none" | "restart"; reason: string };
|
|
@@ -49,6 +56,49 @@ export interface LoginParams {
|
|
|
49
56
|
store?: Pick<ClawChatStore, "upsertActivation">;
|
|
50
57
|
/** Optional database path resolved by the host runtime. */
|
|
51
58
|
dbPath?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Replace this instance's ClawChat identity with a brand-new agent: drop the
|
|
61
|
+
* stored `user_id` so the server mints one for this invite code.
|
|
62
|
+
*/
|
|
63
|
+
newAccount?: boolean;
|
|
64
|
+
/** Re-pair the agent this instance already holds (keeps its identity). */
|
|
65
|
+
repair?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when redeeming an invite code would silently replace a live activation.
|
|
70
|
+
*
|
|
71
|
+
* This channel holds exactly one ClawChat identity — `channels.<id>` is a single
|
|
72
|
+
* slot and `listOpenclawClawlingAccountIds()` returns a single account. Because
|
|
73
|
+
* the stored `user_id` is replayed as a re-pair hint, redeeming a *new* invite
|
|
74
|
+
* code here cannot produce a second agent: the server binds the code to the
|
|
75
|
+
* existing agent, consumes it, creates no second agent and no second contact
|
|
76
|
+
* entry, and the local credentials are overwritten in place — losing the first
|
|
77
|
+
* agent with no error and no warning. Refuse instead, and name the real intents.
|
|
78
|
+
*/
|
|
79
|
+
export class ExistingActivationError extends Error {
|
|
80
|
+
readonly userId: string;
|
|
81
|
+
readonly agentId: string;
|
|
82
|
+
|
|
83
|
+
constructor(userId: string, agentId = "") {
|
|
84
|
+
const who = agentId ? `agent ${agentId} (shadow user ${userId})` : `agent ${userId}`;
|
|
85
|
+
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",
|
|
97
|
+
);
|
|
98
|
+
this.name = "ExistingActivationError";
|
|
99
|
+
this.userId = userId;
|
|
100
|
+
this.agentId = agentId;
|
|
101
|
+
}
|
|
52
102
|
}
|
|
53
103
|
|
|
54
104
|
/**
|
|
@@ -185,6 +235,15 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
185
235
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
186
236
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
187
237
|
|
|
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());
|
|
245
|
+
}
|
|
246
|
+
|
|
188
247
|
const inviteCode = (
|
|
189
248
|
await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()
|
|
190
249
|
).trim();
|
|
@@ -203,8 +262,10 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
203
262
|
|
|
204
263
|
runtime.log("Verifying invite code …");
|
|
205
264
|
let result;
|
|
265
|
+
// A brand-new identity is precisely "do not replay" — the replay is the only
|
|
266
|
+
// thing that would bind this code to the incumbent agent.
|
|
267
|
+
const existingUserId = params.newAccount ? "" : account.userId.trim();
|
|
206
268
|
try {
|
|
207
|
-
const existingUserId = account.userId.trim();
|
|
208
269
|
result = await apiClient.agentsConnect({
|
|
209
270
|
code: inviteCode,
|
|
210
271
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
@@ -213,9 +274,36 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
213
274
|
});
|
|
214
275
|
} catch (err) {
|
|
215
276
|
if (err instanceof ClawlingApiError) {
|
|
216
|
-
|
|
277
|
+
// The stored userId is a re-pair hint, not a credential — the invite
|
|
278
|
+
// code alone decides ownership. AGENT_NOT_FOUND means it names an agent
|
|
279
|
+
// this server has never had (owner account deleted, or the config was
|
|
280
|
+
// repointed at another deployment), so replaying it would fail
|
|
281
|
+
// identically for every future invite code. A 16001 leaves the code
|
|
282
|
+
// `pending` server-side, so shedding the id and retrying once cannot
|
|
283
|
+
// double-spend it. Nothing else is retried: an owner mismatch is a real
|
|
284
|
+
// rejection, and a request that sent no id has no stale state to shed.
|
|
285
|
+
if (existingUserId && err.meta?.code === AGENT_NOT_FOUND_CODE) {
|
|
286
|
+
runtime.log(
|
|
287
|
+
`Stored userId ${existingUserId} no longer exists on this server — retrying as a fresh pairing …`,
|
|
288
|
+
);
|
|
289
|
+
try {
|
|
290
|
+
result = await apiClient.agentsConnect({
|
|
291
|
+
code: inviteCode,
|
|
292
|
+
platform: AGENTS_CONNECT_PLATFORM,
|
|
293
|
+
type: AGENTS_CONNECT_TYPE,
|
|
294
|
+
});
|
|
295
|
+
} catch (retryErr) {
|
|
296
|
+
if (retryErr instanceof ClawlingApiError) {
|
|
297
|
+
throw new Error(`agents/connect failed (${retryErr.kind}): ${retryErr.message}`);
|
|
298
|
+
}
|
|
299
|
+
throw retryErr;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
throw new Error(`agents/connect failed (${err.kind}): ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
throw err;
|
|
217
306
|
}
|
|
218
|
-
throw err;
|
|
219
307
|
}
|
|
220
308
|
|
|
221
309
|
const accessToken = requireConnectString(result?.access_token, "access_token");
|