@clawling/clawchat-plugin-openclaw 2026.7.26-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 +43 -1
- package/package.json +1 -1
- package/src/commands.ts +30 -2
- package/src/login.runtime.ts +55 -1
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
|
}
|
|
@@ -21,6 +21,38 @@ export const AGENTS_CONNECT_TYPE = "clawbot";
|
|
|
21
21
|
* retries once.
|
|
22
22
|
*/
|
|
23
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
|
+
}
|
|
24
56
|
/**
|
|
25
57
|
* Prompt the operator for an invite code.
|
|
26
58
|
*
|
|
@@ -133,6 +165,14 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
133
165
|
// `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
|
|
134
166
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
135
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
|
+
}
|
|
136
176
|
const inviteCode = (await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()).trim();
|
|
137
177
|
if (!inviteCode) {
|
|
138
178
|
throw new Error("Login aborted: invite code is required.");
|
|
@@ -147,7 +187,9 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
147
187
|
});
|
|
148
188
|
runtime.log("Verifying invite code …");
|
|
149
189
|
let result;
|
|
150
|
-
|
|
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();
|
|
151
193
|
try {
|
|
152
194
|
result = await apiClient.agentsConnect({
|
|
153
195
|
code: inviteCode,
|
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
|
@@ -56,6 +56,49 @@ export interface LoginParams {
|
|
|
56
56
|
store?: Pick<ClawChatStore, "upsertActivation">;
|
|
57
57
|
/** Optional database path resolved by the host runtime. */
|
|
58
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
|
+
}
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
/**
|
|
@@ -192,6 +235,15 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
192
235
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
193
236
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
194
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
|
+
|
|
195
247
|
const inviteCode = (
|
|
196
248
|
await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()
|
|
197
249
|
).trim();
|
|
@@ -210,7 +262,9 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
210
262
|
|
|
211
263
|
runtime.log("Verifying invite code …");
|
|
212
264
|
let result;
|
|
213
|
-
|
|
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();
|
|
214
268
|
try {
|
|
215
269
|
result = await apiClient.agentsConnect({
|
|
216
270
|
code: inviteCode,
|