@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.
- package/dist/src/login.runtime.js +75 -19
- package/package.json +1 -1
- package/src/login.runtime.ts +88 -19
|
@@ -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
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
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
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
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 =
|
|
248
|
+
const existingUserId = newAccount ? "" : account.userId.trim();
|
|
193
249
|
try {
|
|
194
250
|
result = await apiClient.agentsConnect({
|
|
195
251
|
code: inviteCode,
|
package/package.json
CHANGED
package/src/login.runtime.ts
CHANGED
|
@@ -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
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
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
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
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 =
|
|
336
|
+
const existingUserId = newAccount ? "" : account.userId.trim();
|
|
268
337
|
try {
|
|
269
338
|
result = await apiClient.agentsConnect({
|
|
270
339
|
code: inviteCode,
|