@clawling/clawchat-plugin-openclaw 2026.9.16-1 → 2026.9.18-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/api-client.js +32 -2
- package/dist/src/connect-check.js +74 -0
- package/dist/src/login.runtime.js +84 -39
- package/dist/src/onboarding-context.js +70 -0
- package/dist/src/onboarding-report.js +50 -0
- package/dist/src/plugin-report.js +1 -0
- package/dist/src/runtime.js +10 -2
- package/dist/src/skill-update.js +1 -1
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +32 -2
- package/skills/manifest.json +6 -6
- package/src/api-client.ts +51 -15
- package/src/api-types.ts +18 -0
- package/src/connect-check.ts +98 -0
- package/src/login.runtime.ts +105 -44
- package/src/onboarding-context.ts +77 -0
- package/src/onboarding-report.ts +60 -0
- package/src/plugin-report.ts +4 -0
- package/src/runtime.ts +11 -3
- package/src/skill-update.ts +1 -1
package/dist/src/api-client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ClawlingApiError, } from "./api-types.js";
|
|
2
2
|
import { CHANNEL_ID } from "./config.js";
|
|
3
3
|
export function buildPluginReportBody(input) {
|
|
4
|
-
|
|
4
|
+
const body = {
|
|
5
5
|
device_id: input.deviceId,
|
|
6
6
|
platform: input.platform,
|
|
7
7
|
plugin_version: input.pluginVersion,
|
|
@@ -9,6 +9,16 @@ export function buildPluginReportBody(input) {
|
|
|
9
9
|
runtime_name: input.runtimeName,
|
|
10
10
|
runtime_version: input.runtimeVersion,
|
|
11
11
|
};
|
|
12
|
+
const ob = input.onboarding;
|
|
13
|
+
if (ob?.wiki_report_id !== undefined)
|
|
14
|
+
body.wiki_report_id = ob.wiki_report_id;
|
|
15
|
+
if (ob?.capability_tier !== undefined)
|
|
16
|
+
body.capability_tier = ob.capability_tier;
|
|
17
|
+
if (ob?.capability_ceiling !== undefined)
|
|
18
|
+
body.capability_ceiling = ob.capability_ceiling;
|
|
19
|
+
if (ob?.capabilities !== undefined)
|
|
20
|
+
body.capabilities = ob.capabilities;
|
|
21
|
+
return body;
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* §A.0 — decode the access token's `exp` claim locally (base64url-decode the
|
|
@@ -472,7 +482,24 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
472
482
|
headers: { "content-type": "application/json" },
|
|
473
483
|
});
|
|
474
484
|
},
|
|
475
|
-
async
|
|
485
|
+
async agentsConnectCheck({ code, platform, user_id: userId, context }) {
|
|
486
|
+
if (!code?.trim()) {
|
|
487
|
+
throw new ClawlingApiError("validation", "agentsConnectCheck: code is required");
|
|
488
|
+
}
|
|
489
|
+
const body = { code: code.trim(), platform: platform.trim() };
|
|
490
|
+
if (userId?.trim())
|
|
491
|
+
body.user_id = userId.trim();
|
|
492
|
+
if (opts.pluginVersion?.trim())
|
|
493
|
+
body.plugin_version = opts.pluginVersion.trim();
|
|
494
|
+
for (const [k, v] of Object.entries(context ?? {}))
|
|
495
|
+
if (v)
|
|
496
|
+
body[k] = v;
|
|
497
|
+
return await call("POST", "/v1/agents/connect/check", {
|
|
498
|
+
headers: { "content-type": "application/json" },
|
|
499
|
+
body: JSON.stringify(body),
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
async agentsConnect({ code: inviteCode, platform, type, user_id: userId, context }) {
|
|
476
503
|
if (!inviteCode?.trim()) {
|
|
477
504
|
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
478
505
|
}
|
|
@@ -493,6 +520,9 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
493
520
|
if (opts.pluginVersion?.trim()) {
|
|
494
521
|
body.plugin_version = opts.pluginVersion.trim();
|
|
495
522
|
}
|
|
523
|
+
for (const [k, v] of Object.entries(context ?? {}))
|
|
524
|
+
if (v)
|
|
525
|
+
body[k] = v;
|
|
496
526
|
return await call("POST", "/v1/agents/connect", {
|
|
497
527
|
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
498
528
|
headers: { "content-type": "application/json" },
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ClawlingApiError } from "./api-types.js";
|
|
2
|
+
import { AGENTS_CONNECT_PLATFORM, RECONNECT_GUIDE_URL } from "./onboarding-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Non-consuming pre-check of a connect code. Records the "checked" funnel
|
|
5
|
+
* stage server-side and tells us whether the code is bound to an existing
|
|
6
|
+
* agent (the reconnect prompt). Any transport-level failure — older backend,
|
|
7
|
+
* rate limit, network — degrades to `null`: the pre-check is telemetry plus a
|
|
8
|
+
* courtesy, never a gate in front of `/connect`.
|
|
9
|
+
*/
|
|
10
|
+
export async function preCheckConnectCode(client, input, log) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await client.agentsConnectCheck({
|
|
13
|
+
code: input.code,
|
|
14
|
+
platform: AGENTS_CONNECT_PLATFORM,
|
|
15
|
+
...(input.userId ? { user_id: input.userId } : {}),
|
|
16
|
+
...(input.context ? { context: input.context } : {}),
|
|
17
|
+
});
|
|
18
|
+
// A 200 with no data says nothing about the code: degrade exactly like an
|
|
19
|
+
// unreachable endpoint (Hermes' `evaluate_precheck({})` does the same).
|
|
20
|
+
if (!res || typeof res !== "object" || Object.keys(res).length === 0) {
|
|
21
|
+
log("Connect-code pre-check returned no data; continuing without it.");
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
pairable: res.pairable === true,
|
|
26
|
+
status: typeof res.status === "string" ? res.status : "",
|
|
27
|
+
boundAgent: res.bound_agent === true,
|
|
28
|
+
userIdStatus: typeof res.user_id_status === "string" ? res.user_id_status : "",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const kind = err instanceof ClawlingApiError ? err.kind : "error";
|
|
33
|
+
log(`Connect-code pre-check unavailable (${kind}); continuing without it.`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Owner-facing explanation for `pairable: false`. No flags, no minutes, one URL. */
|
|
38
|
+
export function unpairableMessage(pre) {
|
|
39
|
+
if (pre.userIdStatus === "owner_mismatch" && pre.boundAgent) {
|
|
40
|
+
// The server cannot tell us whether the bound agent shares this identity's
|
|
41
|
+
// owner, only that it is a different agent — so say exactly that.
|
|
42
|
+
return ("This connect code is the reconnect prompt for a different agent than the identity stored here. " +
|
|
43
|
+
"Ask your owner to send the reconnect prompt from THIS agent's chat in the ClawChat app.");
|
|
44
|
+
}
|
|
45
|
+
if (pre.userIdStatus === "invalid") {
|
|
46
|
+
return ("The identity stored here is not a valid ClawChat user id, so it cannot be restored. " +
|
|
47
|
+
"Activate as a brand-new agent with --new-account.");
|
|
48
|
+
}
|
|
49
|
+
if (pre.userIdStatus === "owner_mismatch") {
|
|
50
|
+
return ("This connect code belongs to a different ClawChat account than the identity stored here. " +
|
|
51
|
+
"Ask the owner of THIS agent for a code, or activate as a brand-new agent with --new-account.");
|
|
52
|
+
}
|
|
53
|
+
switch (pre.status) {
|
|
54
|
+
case "paired":
|
|
55
|
+
return ("This connect code was already redeemed. If this agent lost its connection, ask your owner " +
|
|
56
|
+
`to send you the reconnect prompt from the ClawChat app and follow ${RECONNECT_GUIDE_URL}; ` +
|
|
57
|
+
"otherwise ask for a fresh code.");
|
|
58
|
+
case "expired":
|
|
59
|
+
case "invalid":
|
|
60
|
+
return `This connect code is ${pre.status}. Ask your owner for a fresh code from the ClawChat app.`;
|
|
61
|
+
default:
|
|
62
|
+
return `This connect code is not pairable (status=${pre.status || "unknown"}). Ask your owner for a fresh code from the ClawChat app.`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Refusal for a new-identity intent (`--new-account` or the interactive
|
|
67
|
+
* choice) on a bound code: without a user_id, `/connect` restores the bound
|
|
68
|
+
* agent instead of creating one. No flags, no minutes, no URL.
|
|
69
|
+
*/
|
|
70
|
+
export function boundCodeNewIdentityMessage() {
|
|
71
|
+
return ("This is a reconnect code bound to an existing agent, so it cannot create a new agent. " +
|
|
72
|
+
"Ask your owner for a normal connect code from the ClawChat app, or use this reconnect prompt " +
|
|
73
|
+
"on the agent it belongs to.");
|
|
74
|
+
}
|
|
@@ -6,23 +6,12 @@ import { openclawClawlingAccountSlotPath, patchOpenclawClawlingAccountSection }
|
|
|
6
6
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
7
7
|
import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
8
8
|
import { getClawChatStore } from "./storage.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Agent type tag sent to `/v1/agents/connect`. The clawchat channel is
|
|
16
|
-
* always a bot; humans don't log in through this flow.
|
|
17
|
-
*/
|
|
18
|
-
export const AGENTS_CONNECT_TYPE = "clawbot";
|
|
19
|
-
/**
|
|
20
|
-
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
21
|
-
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
22
|
-
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
23
|
-
* retries once.
|
|
24
|
-
*/
|
|
25
|
-
export const AGENT_NOT_FOUND_CODE = 16001;
|
|
9
|
+
import { buildOnboardingContext, RECONNECT_GUIDE_URL } from "./onboarding-context.js";
|
|
10
|
+
import { boundCodeNewIdentityMessage, preCheckConnectCode, unpairableMessage } from "./connect-check.js";
|
|
11
|
+
// Re-exported: `src/login.runtime.test.ts` and `src/commands.ts` import these
|
|
12
|
+
// from here, so the historical import path keeps working after the move.
|
|
13
|
+
import { AGENTS_CONNECT_PLATFORM, AGENTS_CONNECT_TYPE, AGENT_NOT_FOUND_CODE, } from "./onboarding-context.js";
|
|
14
|
+
export { AGENTS_CONNECT_PLATFORM, AGENTS_CONNECT_TYPE, AGENT_NOT_FOUND_CODE };
|
|
26
15
|
/**
|
|
27
16
|
* Thrown when redeeming an invite code would silently replace a live activation.
|
|
28
17
|
*
|
|
@@ -43,9 +32,11 @@ export class ExistingActivationError extends Error {
|
|
|
43
32
|
"spent. Re-run stating the intent:\n" +
|
|
44
33
|
" - Pair as a BRAND-NEW agent (this instance stops using the identity " +
|
|
45
34
|
"above): /clawchat-activate <CODE> --new-account\n" +
|
|
46
|
-
" - RESTORE the identity above
|
|
47
|
-
"
|
|
48
|
-
"
|
|
35
|
+
" - RESTORE the identity above: do not spend a fresh code on it. Ask your " +
|
|
36
|
+
"owner to send you the reconnect prompt from the ClawChat app — its code is " +
|
|
37
|
+
"bound to this identity and usually restores it on its own; if activation " +
|
|
38
|
+
"still reports it as already paired, run it again with --repair " +
|
|
39
|
+
`(/clawchat-activate <CODE> --repair) — and follow ${RECONNECT_GUIDE_URL}\n` +
|
|
49
40
|
" - To run a SECOND agent alongside this one instead, activate it as a " +
|
|
50
41
|
"named account: /clawchat-activate <CODE> --account <name> (or: openclaw " +
|
|
51
42
|
"channels add --channel clawchat-plugin-openclaw --account <name> --token <CODE>)");
|
|
@@ -197,7 +188,50 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
197
188
|
// `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
|
|
198
189
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
199
190
|
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
200
|
-
|
|
191
|
+
const inviteCode = (await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()).trim();
|
|
192
|
+
if (!inviteCode) {
|
|
193
|
+
throw new Error("Login aborted: invite code is required.");
|
|
194
|
+
}
|
|
195
|
+
const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
|
|
196
|
+
baseUrl: account.baseUrl,
|
|
197
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
198
|
+
// Pre-login we may not have a token yet. Send the current one (or empty)
|
|
199
|
+
// — the server should accept an unauthenticated invite-code exchange.
|
|
200
|
+
token: account.token || "",
|
|
201
|
+
pluginVersion: resolvePluginVersion(),
|
|
202
|
+
});
|
|
203
|
+
// Telemetry the backend joins with wiki field reports; ignored by older servers.
|
|
204
|
+
const onboarding = buildOnboardingContext();
|
|
205
|
+
// Non-consuming pre-check. `null` = endpoint unavailable, carry on. A bound
|
|
206
|
+
// code (the owner's reconnect prompt) is the owner's own proof of intent, so
|
|
207
|
+
// it settles the "new agent or restore?" question below without a flag.
|
|
208
|
+
//
|
|
209
|
+
// The pre-check only carries the `user_id` that `/connect` would replay. An
|
|
210
|
+
// explicit `newAccount` never replays it, so it must not be judged against
|
|
211
|
+
// it either: the server marks `pairable:false` for an `owner_mismatch` /
|
|
212
|
+
// `invalid` id, and that would block the very escape the refusal recommends.
|
|
213
|
+
const storedUserId = account.userId.trim();
|
|
214
|
+
const precheckUserId = params.newAccount ? "" : storedUserId;
|
|
215
|
+
runtime.log("Checking the invite code …");
|
|
216
|
+
let precheck = await preCheckConnectCode(apiClient, { code: inviteCode, userId: precheckUserId || undefined, context: onboarding }, runtime.log);
|
|
217
|
+
// Whether the operator will be asked "new agent or restore?" below. When
|
|
218
|
+
// they will, a refusal caused only by the stored identity (not by the code)
|
|
219
|
+
// is deferred: choosing a brand-new identity drops that id, so the verdict
|
|
220
|
+
// on it no longer applies.
|
|
221
|
+
const identityUnsettled = storedUserId !== "" && account.token.trim() !== "" && !params.newAccount && !params.repair;
|
|
222
|
+
const refusedForIdentity = precheck !== null &&
|
|
223
|
+
!precheck.pairable &&
|
|
224
|
+
precheckUserId !== "" &&
|
|
225
|
+
// Only a live code: a dead one (expired / paired / invalid) refuses on its
|
|
226
|
+
// own account whatever identity is chosen, so there is nothing to defer.
|
|
227
|
+
precheck.status === "pending" &&
|
|
228
|
+
(precheck.userIdStatus === "owner_mismatch" || precheck.userIdStatus === "invalid");
|
|
229
|
+
const deferredRefusal = identityUnsettled && refusedForIdentity ? precheck : null;
|
|
230
|
+
if (precheck && !precheck.pairable && !deferredRefusal) {
|
|
231
|
+
throw new Error(unpairableMessage(precheck));
|
|
232
|
+
}
|
|
233
|
+
const boundToIncumbent = precheck?.pairable === true && precheck.boundAgent === true && precheckUserId !== "";
|
|
234
|
+
// Fork before the invite code is spent, so neither branch spends the code
|
|
201
235
|
// before the outcome is settled. "Live" is decided by whether a usable token
|
|
202
236
|
// remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
|
|
203
237
|
// logged-out instance still re-pairs with no flag at all, which is the flow
|
|
@@ -207,46 +241,56 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
207
241
|
// ambiguous, because redeeming a code here can either mint a new agent or
|
|
208
242
|
// re-pair the incumbent one. Ask which, and only refuse when nobody answers.
|
|
209
243
|
// An explicit `newAccount` / `repair` from the caller has already settled it,
|
|
210
|
-
// so no prompt.
|
|
244
|
+
// so no prompt. Nor is one needed when the pre-check proved the code is the
|
|
245
|
+
// owner's own reconnect prompt for this very agent (`boundToIncumbent`) — a
|
|
246
|
+
// bound code can only ever restore the incumbent identity.
|
|
211
247
|
let newAccount = params.newAccount;
|
|
212
|
-
if (
|
|
248
|
+
if (identityUnsettled && !boundToIncumbent) {
|
|
213
249
|
const identity = account.agentId.trim()
|
|
214
|
-
? `agent ${account.agentId.trim()} (shadow user ${
|
|
215
|
-
: `agent ${
|
|
250
|
+
? `agent ${account.agentId.trim()} (shadow user ${storedUserId})`
|
|
251
|
+
: `agent ${storedUserId}`;
|
|
216
252
|
const intent = await (params.readActivationIntent ??
|
|
217
253
|
(() => promptActivationIntentFromStdin(runtime, identity)))();
|
|
218
254
|
// Only "new-account" changes what gets sent. Restoring needs no flag:
|
|
219
255
|
// replaying the stored `user_id` IS the re-pair, and that is already the
|
|
220
256
|
// default below — so choosing it simply means "don't refuse".
|
|
221
|
-
if (intent === "new-account")
|
|
257
|
+
if (intent === "new-account") {
|
|
222
258
|
newAccount = true;
|
|
259
|
+
if (deferredRefusal) {
|
|
260
|
+
// The first verdict judged the id we are now dropping; re-check the
|
|
261
|
+
// code on its own before spending it.
|
|
262
|
+
precheck = await preCheckConnectCode(apiClient, { code: inviteCode, context: onboarding }, runtime.log);
|
|
263
|
+
if (precheck && !precheck.pairable) {
|
|
264
|
+
throw new Error(unpairableMessage(precheck));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (deferredRefusal) {
|
|
269
|
+
// Restoring (or nobody to ask) replays the id the server already refused.
|
|
270
|
+
throw new Error(unpairableMessage(deferredRefusal));
|
|
271
|
+
}
|
|
223
272
|
else if (intent === null) {
|
|
224
|
-
throw new ExistingActivationError(
|
|
273
|
+
throw new ExistingActivationError(storedUserId, account.agentId.trim());
|
|
225
274
|
}
|
|
226
275
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
276
|
+
// A new identity (flag or interactive choice) sends no user_id, and /connect
|
|
277
|
+
// without one on a bound code silently RESTORES the bound agent and spends
|
|
278
|
+
// the code. A reconnect code can never mint an agent, so refuse first.
|
|
279
|
+
if (newAccount && precheck?.boundAgent === true) {
|
|
280
|
+
throw new Error(boundCodeNewIdentityMessage());
|
|
230
281
|
}
|
|
231
|
-
const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
|
|
232
|
-
baseUrl: account.baseUrl,
|
|
233
|
-
mediaBaseUrl: account.mediaBaseUrl,
|
|
234
|
-
// Pre-login we may not have a token yet. Send the current one (or empty)
|
|
235
|
-
// — the server should accept an unauthenticated invite-code exchange.
|
|
236
|
-
token: account.token || "",
|
|
237
|
-
pluginVersion: resolvePluginVersion(),
|
|
238
|
-
});
|
|
239
282
|
runtime.log("Verifying invite code …");
|
|
240
283
|
let result;
|
|
241
284
|
// A brand-new identity is precisely "do not replay" — the replay is the only
|
|
242
285
|
// thing that would bind this code to the incumbent agent.
|
|
243
|
-
const existingUserId = newAccount ? "" :
|
|
286
|
+
const existingUserId = newAccount ? "" : storedUserId;
|
|
244
287
|
try {
|
|
245
288
|
result = await apiClient.agentsConnect({
|
|
246
289
|
code: inviteCode,
|
|
247
290
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
248
291
|
type: AGENTS_CONNECT_TYPE,
|
|
249
292
|
...(existingUserId ? { user_id: existingUserId } : {}),
|
|
293
|
+
context: onboarding,
|
|
250
294
|
});
|
|
251
295
|
}
|
|
252
296
|
catch (err) {
|
|
@@ -266,6 +310,7 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
266
310
|
code: inviteCode,
|
|
267
311
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
268
312
|
type: AGENTS_CONNECT_TYPE,
|
|
313
|
+
context: onboarding,
|
|
269
314
|
});
|
|
270
315
|
}
|
|
271
316
|
catch (retryErr) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding telemetry the plugin attaches to `/v1/agents/connect` and
|
|
3
|
+
* `/v1/agents/connect/check` (all optional, all ignored by older backends).
|
|
4
|
+
*
|
|
5
|
+
* Values mirror the connection wiki's manifest vocabulary so the backend can
|
|
6
|
+
* join plugin activations with wiki field reports without an alias table:
|
|
7
|
+
* - `agent_kind` = the wiki `match.agent` name for this host
|
|
8
|
+
* - `lane` = "self": this plugin holds its own WebSocket + token
|
|
9
|
+
* - `matched_install` = the wiki page slug that routes to this plugin
|
|
10
|
+
* Nothing here is a credential and nothing here gates activation.
|
|
11
|
+
*/
|
|
12
|
+
export const AGENT_KIND = "openclaw";
|
|
13
|
+
export const LANE = "self";
|
|
14
|
+
export const MATCHED_INSTALL = "install/official-openclaw";
|
|
15
|
+
/** Public connection wiki. `/start.md` is the entry page; `/reconnect.md` the re-pair page. */
|
|
16
|
+
export const START_GUIDE_URL = "https://agent-connection.clawling.com/start.md";
|
|
17
|
+
export const RECONNECT_GUIDE_URL = "https://agent-connection.clawling.com/reconnect.md";
|
|
18
|
+
const WIKI_VERSION_MAX = 40;
|
|
19
|
+
/** Wiki `os` enum (windows | macos | linux); "" for anything else so callers omit it. */
|
|
20
|
+
export function hostOs(plat = process.platform) {
|
|
21
|
+
switch (plat) {
|
|
22
|
+
case "win32":
|
|
23
|
+
return "windows";
|
|
24
|
+
case "darwin":
|
|
25
|
+
return "macos";
|
|
26
|
+
case "linux":
|
|
27
|
+
return "linux";
|
|
28
|
+
default:
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The wiki build stamp the agent saw on the install page. The official install
|
|
34
|
+
* page tells the agent to export `CLAWCHAT_WIKI_VERSION` before activating; a
|
|
35
|
+
* hand-run `openclaw channels add` simply has none.
|
|
36
|
+
*/
|
|
37
|
+
export function wikiVersion(env = process.env) {
|
|
38
|
+
return (env.CLAWCHAT_WIKI_VERSION ?? "").trim().slice(0, WIKI_VERSION_MAX);
|
|
39
|
+
}
|
|
40
|
+
export function buildOnboardingContext(opts = {}) {
|
|
41
|
+
const out = {
|
|
42
|
+
agent_kind: AGENT_KIND,
|
|
43
|
+
lane: LANE,
|
|
44
|
+
matched_install: MATCHED_INSTALL,
|
|
45
|
+
};
|
|
46
|
+
const os = hostOs(opts.platform);
|
|
47
|
+
if (os)
|
|
48
|
+
out.os = os;
|
|
49
|
+
const wiki = wikiVersion(opts.env);
|
|
50
|
+
if (wiki)
|
|
51
|
+
out.wiki_version = wiki;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
56
|
+
* agent runtime — openclaw's bundled clawchat channel.
|
|
57
|
+
*/
|
|
58
|
+
export const AGENTS_CONNECT_PLATFORM = "openclaw";
|
|
59
|
+
/**
|
|
60
|
+
* Agent type tag sent to `/v1/agents/connect`. The clawchat channel is
|
|
61
|
+
* always a bot; humans don't log in through this flow.
|
|
62
|
+
*/
|
|
63
|
+
export const AGENTS_CONNECT_TYPE = "clawbot";
|
|
64
|
+
/**
|
|
65
|
+
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
66
|
+
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
67
|
+
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
68
|
+
* retries once.
|
|
69
|
+
*/
|
|
70
|
+
export const AGENT_NOT_FOUND_CODE = 16001;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const REPORT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
5
|
+
const CAP_KEYS = ["headless", "mcp", "permission_hook", "session_line"];
|
|
6
|
+
function tier(v) {
|
|
7
|
+
return Number.isInteger(v) && v >= 1 && v <= 4 ? v : undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read + validate `~/clawchat/onboarding.json`. Never throws: an unresolvable
|
|
11
|
+
* home directory, an absent/unreadable file, or malformed JSON all fall
|
|
12
|
+
* through to `null`, same as a file with no valid fields — this must be safe
|
|
13
|
+
* to call unconditionally from a best-effort report path.
|
|
14
|
+
*/
|
|
15
|
+
export function readOnboardingReport(homeDir) {
|
|
16
|
+
try {
|
|
17
|
+
const base = homeDir ?? os.homedir();
|
|
18
|
+
const file = path.join(base, "clawchat", "onboarding.json");
|
|
19
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
20
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
21
|
+
return null;
|
|
22
|
+
const o = raw;
|
|
23
|
+
const out = {};
|
|
24
|
+
if (typeof o.wiki_report_id === "string" && REPORT_ID_RE.test(o.wiki_report_id))
|
|
25
|
+
out.wiki_report_id = o.wiki_report_id;
|
|
26
|
+
const t = tier(o.capability_tier);
|
|
27
|
+
if (t !== undefined)
|
|
28
|
+
out.capability_tier = t;
|
|
29
|
+
const c = tier(o.capability_ceiling);
|
|
30
|
+
// The backend rejects the WHOLE report (22004) when ceiling < tier, which
|
|
31
|
+
// would silently stop the version row updating. Keep the tier, drop the
|
|
32
|
+
// inconsistent ceiling.
|
|
33
|
+
if (c !== undefined && (t === undefined || c >= t))
|
|
34
|
+
out.capability_ceiling = c;
|
|
35
|
+
if (o.capabilities && typeof o.capabilities === "object") {
|
|
36
|
+
const caps = {};
|
|
37
|
+
for (const k of CAP_KEYS) {
|
|
38
|
+
const v = o.capabilities[k];
|
|
39
|
+
if (typeof v === "boolean")
|
|
40
|
+
caps[k] = v;
|
|
41
|
+
}
|
|
42
|
+
if (Object.keys(caps).length > 0)
|
|
43
|
+
out.capabilities = caps;
|
|
44
|
+
}
|
|
45
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -7,10 +7,12 @@ import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
|
7
7
|
import { buildActivationBootstrapText } from "./activation-greeting.js";
|
|
8
8
|
import { buildFriendGreetingEnvelope } from "./friend-greeting.js";
|
|
9
9
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
10
|
+
import { readOnboardingReport } from "./onboarding-report.js";
|
|
10
11
|
import { ensureLivewareCli, livewareCliHomeDir, livewareSampleRootDir, resolveLivewarePath, } from "./liveware-cli.js";
|
|
11
12
|
import { LivewareSampleSupervisor, } from "./liveware-sample.js";
|
|
12
13
|
import { ClawlingApiError } from "./api-types.js";
|
|
13
14
|
import { RefreshManager } from "./refresh-manager.js";
|
|
15
|
+
import { RECONNECT_GUIDE_URL } from "./onboarding-context.js";
|
|
14
16
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
15
17
|
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
|
|
16
18
|
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
|
|
@@ -102,8 +104,12 @@ const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
|
|
|
102
104
|
]);
|
|
103
105
|
const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
|
|
104
106
|
// §C.1 — user-visible message emitted on permanent token expiry. Kept
|
|
105
|
-
// byte-identical to the Hermes plugin (parity spec §C.1.4).
|
|
106
|
-
|
|
107
|
+
// byte-identical to the Hermes plugin (parity spec §C.1.4). Points the owner
|
|
108
|
+
// at the reconnect prompt (a code bound to this identity) rather than at a
|
|
109
|
+
// slash command: a fresh create code spent here would mint a second agent.
|
|
110
|
+
export const CLAWCHAT_TOKEN_EXPIRED_MESSAGE = "ClawChat token expired and could not be refreshed. Ask your owner to send you the reconnect prompt from the ClawChat app, then follow " +
|
|
111
|
+
RECONNECT_GUIDE_URL +
|
|
112
|
+
".";
|
|
107
113
|
const CLAWCHAT_TOKEN_EXPIRED_LAST_ERROR = "token expired — re-pair required";
|
|
108
114
|
function isRecord(value) {
|
|
109
115
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -724,6 +730,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
724
730
|
pluginVersion,
|
|
725
731
|
agentVersion,
|
|
726
732
|
authenticated: false,
|
|
733
|
+
onboarding: readOnboardingReport(),
|
|
727
734
|
log,
|
|
728
735
|
});
|
|
729
736
|
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
@@ -796,6 +803,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
796
803
|
pluginVersion,
|
|
797
804
|
agentVersion,
|
|
798
805
|
authenticated: true,
|
|
806
|
+
onboarding: readOnboardingReport(),
|
|
799
807
|
log,
|
|
800
808
|
});
|
|
801
809
|
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
package/dist/src/skill-update.js
CHANGED
|
@@ -66,7 +66,7 @@ export const OFFICIAL_SKILLS_BASE = "https://raw.githubusercontent.com/clawling/
|
|
|
66
66
|
* in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
|
|
67
67
|
* imports the same ref, so the `livewares` tree at that tag is pinned too.
|
|
68
68
|
*/
|
|
69
|
-
export const DEFAULT_SKILLS_REF = "skills-v1.
|
|
69
|
+
export const DEFAULT_SKILLS_REF = "skills-v1.11.0";
|
|
70
70
|
/** Refuse to treat an absurdly large response as a skill file (defence in depth). */
|
|
71
71
|
export const MAX_SKILL_BYTES = 256 * 1024;
|
|
72
72
|
/** This adapter's host target inside `skills/manifest.json`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-core
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.5.0
|
|
4
4
|
description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, sending an image, file, or voice/audio clip into a conversation, or plugin install/update/activation.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -44,7 +44,14 @@ If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the r
|
|
|
44
44
|
|
|
45
45
|
### What a connect code is
|
|
46
46
|
|
|
47
|
-
A connect code is issued by the owner
|
|
47
|
+
A connect code is issued by the owner in the ClawChat app's add-agent card and
|
|
48
|
+
usually reaches you inside the connection prompt the app produces, which points
|
|
49
|
+
you at https://agent-connection.clawling.com/start.md. It is a short single-use
|
|
50
|
+
string that expires on its own. Use it **exactly as provided** — do not lowercase,
|
|
51
|
+
normalize, add prefixes, invent, reuse, or retry a code, and never assume a length
|
|
52
|
+
or a digits-only shape: an older note here called it a six-digit "invite code";
|
|
53
|
+
that was the legacy pairing flow and no longer holds. If activation fails with a
|
|
54
|
+
non-zero exit or API error, report the error verbatim and ask for a fresh code.
|
|
48
55
|
|
|
49
56
|
### When this OpenClaw is already connected
|
|
50
57
|
|
|
@@ -57,6 +64,27 @@ A fresh connect code always means **one more identity**. If the owner hands you
|
|
|
57
64
|
|
|
58
65
|
Never pick a flag by matching words in an error message, and never re-run activation with a flag you chose to get past one.
|
|
59
66
|
|
|
67
|
+
### Lost the connection (token expired, auto-logout, or an "already paired" refusal)
|
|
68
|
+
|
|
69
|
+
Do not spend a fresh create code on the identity you already hold — that mints a
|
|
70
|
+
second agent. Ask your owner to send you the **reconnect prompt** from the ClawChat
|
|
71
|
+
app (the agent's own page) and follow
|
|
72
|
+
https://agent-connection.clawling.com/reconnect.md. The code in that prompt is
|
|
73
|
+
bound to your identity: activation usually restores it on its own, and if it
|
|
74
|
+
still reports the identity as already paired, run it again with `--repair`
|
|
75
|
+
(`/clawchat-activate CODE --repair`).
|
|
76
|
+
|
|
77
|
+
### After onboarding: the field report
|
|
78
|
+
|
|
79
|
+
If your owner sent you here from the ClawChat app, file the structured field
|
|
80
|
+
report described in https://agent-connection.clawling.com/start.md Appendix B once
|
|
81
|
+
you have exchanged one real message with your owner in both directions. Keep the
|
|
82
|
+
returned `id` and write it to `~/clawchat/onboarding.json` as
|
|
83
|
+
`{"wiki_report_id": "<id>"}` (plain JSON, no other keys required); the plugin
|
|
84
|
+
forwards it to ClawChat on its next connection so the owner's app can show that
|
|
85
|
+
the report exists. Never put a ClawChat user, agent, or conversation id in the
|
|
86
|
+
report itself.
|
|
87
|
+
|
|
60
88
|
## Output Visibility
|
|
61
89
|
|
|
62
90
|
When the user asks to change ClawChat output verbosity, use the runtime slash command for the current conversation. Treat natural-language wording as aliases for the three supported modes:
|
|
@@ -101,6 +129,8 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
|
|
|
101
129
|
|
|
102
130
|
## Profile And Identity Sync
|
|
103
131
|
|
|
132
|
+
**A rename is a profile edit, not a note to self.** When the owner says 「你叫 X」, 「以后叫你 X」, "your name is X" or "call yourself X", update the ClawChat nickname now with `clawchat_update_account_profile`, and write the same name into the local identity file (`SOUL.md` or `soul.md`) so the two stay coherent. Then confirm with the name as it now appears in their contacts. Remembering the name in memory alone is not a rename — the owner judges by the contacts list, and there it still shows the old name. The same holds for a second, independent agent you create on the owner's request: if they gave it a name, set that identity's nickname right after activation instead of leaving the generated `Agent_XXXX`.
|
|
133
|
+
|
|
104
134
|
When updating the OpenClaw agent identity file, such as `SOUL.md` or `soul.md`, also update the configured ClawChat account profile when the changed field is shown on the ClawChat profile:
|
|
105
135
|
|
|
106
136
|
```mermaid
|
package/skills/manifest.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"skills": {
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"clawchat-core": {
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.5.0",
|
|
7
7
|
"path": "openclaw/clawchat-core/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
8
|
+
"sha256": "911e18c019a13d65174a7fc591a1f862fd3de1b1f461bca4a6d2b703f2b82bd6",
|
|
9
|
+
"bytes": 13624
|
|
10
10
|
},
|
|
11
11
|
"clawchat-liveware": {
|
|
12
12
|
"version": "1.2.2",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
},
|
|
36
36
|
"hermes": {
|
|
37
37
|
"clawchat-core": {
|
|
38
|
-
"version": "1.
|
|
38
|
+
"version": "1.11.0",
|
|
39
39
|
"path": "hermes/clawchat-core/SKILL.md",
|
|
40
|
-
"sha256": "
|
|
41
|
-
"bytes":
|
|
40
|
+
"sha256": "fadc82be5cfd43d6ccedcf7ec9b3062fa15f6eeea9fd556f4c35c3f9488e22a1",
|
|
41
|
+
"bytes": 20784
|
|
42
42
|
},
|
|
43
43
|
"clawchat-liveware": {
|
|
44
44
|
"version": "1.2.2",
|
package/src/api-client.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawlingApiError,
|
|
3
3
|
type AgentAppView,
|
|
4
|
+
type AgentConnectCheckInput,
|
|
5
|
+
type AgentConnectCheckResult,
|
|
4
6
|
type AgentMetadataPatch,
|
|
5
7
|
type AgentProfile,
|
|
6
8
|
type AgentConnectResult,
|
|
@@ -17,9 +19,24 @@ import {
|
|
|
17
19
|
type UserSearchHit,
|
|
18
20
|
} from "./api-types.ts";
|
|
19
21
|
import type { GroupSettings, GroupSettingsFetchResult } from "./group-settings.ts";
|
|
22
|
+
import type { OnboardingReportFields } from "./onboarding-report.ts";
|
|
20
23
|
import type { PermissionPolicy, PermState } from "./permissions.ts";
|
|
21
24
|
import { CHANNEL_ID } from "./config.ts";
|
|
22
25
|
|
|
26
|
+
/** Params for `agentsConnect` — exchange an invite code for an agent token. */
|
|
27
|
+
export interface AgentsConnectInput {
|
|
28
|
+
/** The invite code entered by the operator. */
|
|
29
|
+
code: string;
|
|
30
|
+
/** Platform the agent is attaching from (e.g. "openclaw"). */
|
|
31
|
+
platform: string;
|
|
32
|
+
/** Agent type tag (e.g. "bot"). */
|
|
33
|
+
type: string;
|
|
34
|
+
/** Existing configured ClawChat user id, when re-activating an account. */
|
|
35
|
+
user_id?: string;
|
|
36
|
+
/** agent_kind / os / lane / wiki_version / matched_install — see onboarding-context.ts */
|
|
37
|
+
context?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
export interface PluginReportInput {
|
|
24
41
|
deviceId: string;
|
|
25
42
|
platform: string;
|
|
@@ -27,10 +44,12 @@ export interface PluginReportInput {
|
|
|
27
44
|
agentVersion: string;
|
|
28
45
|
runtimeName: string;
|
|
29
46
|
runtimeVersion: string;
|
|
47
|
+
/** Agent-written onboarding facts (`~/clawchat/onboarding.json`); optional. */
|
|
48
|
+
onboarding?: OnboardingReportFields | null;
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
export function buildPluginReportBody(input: PluginReportInput): Record<string,
|
|
33
|
-
|
|
51
|
+
export function buildPluginReportBody(input: PluginReportInput): Record<string, unknown> {
|
|
52
|
+
const body: Record<string, unknown> = {
|
|
34
53
|
device_id: input.deviceId,
|
|
35
54
|
platform: input.platform,
|
|
36
55
|
plugin_version: input.pluginVersion,
|
|
@@ -38,6 +57,12 @@ export function buildPluginReportBody(input: PluginReportInput): Record<string,
|
|
|
38
57
|
runtime_name: input.runtimeName,
|
|
39
58
|
runtime_version: input.runtimeVersion,
|
|
40
59
|
};
|
|
60
|
+
const ob = input.onboarding;
|
|
61
|
+
if (ob?.wiki_report_id !== undefined) body.wiki_report_id = ob.wiki_report_id;
|
|
62
|
+
if (ob?.capability_tier !== undefined) body.capability_tier = ob.capability_tier;
|
|
63
|
+
if (ob?.capability_ceiling !== undefined) body.capability_ceiling = ob.capability_ceiling;
|
|
64
|
+
if (ob?.capabilities !== undefined) body.capabilities = ob.capabilities;
|
|
65
|
+
return body;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
export interface ApiClientOptions {
|
|
@@ -149,19 +174,16 @@ export interface OpenclawClawlingApiClient {
|
|
|
149
174
|
uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
|
|
150
175
|
/**
|
|
151
176
|
* Exchange an invite code for an agent token.
|
|
152
|
-
* Request body shape: `{ code, platform, type, user_id?, plugin_version? }`.
|
|
153
|
-
* `plugin_version` is included when `ApiClientOptions.pluginVersion` is set
|
|
177
|
+
* Request body shape: `{ code, platform, type, user_id?, plugin_version?, agent_kind?, os?, lane?, wiki_version?, matched_install? }`.
|
|
178
|
+
* `plugin_version` is included when `ApiClientOptions.pluginVersion` is set;
|
|
179
|
+
* `context` is flattened onto the top-level body (see `onboarding-context.ts`).
|
|
180
|
+
*/
|
|
181
|
+
agentsConnect(params: AgentsConnectInput): Promise<AgentConnectResult>;
|
|
182
|
+
/**
|
|
183
|
+
* POST /v1/agents/connect/check — non-consuming pairability pre-check. Same
|
|
184
|
+
* `X-Device-Id` as `agentsConnect` so the funnel row is keyed identically.
|
|
154
185
|
*/
|
|
155
|
-
|
|
156
|
-
/** The invite code entered by the operator. */
|
|
157
|
-
code: string;
|
|
158
|
-
/** Platform the agent is attaching from (e.g. "openclaw"). */
|
|
159
|
-
platform: string;
|
|
160
|
-
/** Agent type tag (e.g. "bot"). */
|
|
161
|
-
type: string;
|
|
162
|
-
/** Existing configured ClawChat user id, when re-activating an account. */
|
|
163
|
-
user_id?: string;
|
|
164
|
-
}): Promise<AgentConnectResult>;
|
|
186
|
+
agentsConnectCheck(input: AgentConnectCheckInput): Promise<AgentConnectCheckResult>;
|
|
165
187
|
/**
|
|
166
188
|
* Upload an avatar image via `POST /v1/files/upload-url`. The resulting
|
|
167
189
|
* `url` is what you then pass to `updateMyProfile({ avatar_url: url })`.
|
|
@@ -755,7 +777,20 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
755
777
|
},
|
|
756
778
|
);
|
|
757
779
|
},
|
|
758
|
-
async
|
|
780
|
+
async agentsConnectCheck({ code, platform, user_id: userId, context }): Promise<AgentConnectCheckResult> {
|
|
781
|
+
if (!code?.trim()) {
|
|
782
|
+
throw new ClawlingApiError("validation", "agentsConnectCheck: code is required");
|
|
783
|
+
}
|
|
784
|
+
const body: Record<string, string> = { code: code.trim(), platform: platform.trim() };
|
|
785
|
+
if (userId?.trim()) body.user_id = userId.trim();
|
|
786
|
+
if (opts.pluginVersion?.trim()) body.plugin_version = opts.pluginVersion.trim();
|
|
787
|
+
for (const [k, v] of Object.entries(context ?? {})) if (v) body[k] = v;
|
|
788
|
+
return await call<AgentConnectCheckResult>("POST", "/v1/agents/connect/check", {
|
|
789
|
+
headers: { "content-type": "application/json" },
|
|
790
|
+
body: JSON.stringify(body),
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
async agentsConnect({ code: inviteCode, platform, type, user_id: userId, context }): Promise<AgentConnectResult> {
|
|
759
794
|
if (!inviteCode?.trim()) {
|
|
760
795
|
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
761
796
|
}
|
|
@@ -776,6 +811,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
776
811
|
if (opts.pluginVersion?.trim()) {
|
|
777
812
|
body.plugin_version = opts.pluginVersion.trim();
|
|
778
813
|
}
|
|
814
|
+
for (const [k, v] of Object.entries(context ?? {})) if (v) body[k] = v;
|
|
779
815
|
return await call<AgentConnectResult>("POST", "/v1/agents/connect", {
|
|
780
816
|
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
781
817
|
headers: { "content-type": "application/json" },
|
package/src/api-types.ts
CHANGED
|
@@ -146,6 +146,24 @@ export interface AgentConnectResult {
|
|
|
146
146
|
conversation?: AgentConnectConversation;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** Body of POST /v1/agents/connect/check. Same optional telemetry as /connect. */
|
|
150
|
+
export interface AgentConnectCheckInput {
|
|
151
|
+
code: string;
|
|
152
|
+
platform: string;
|
|
153
|
+
user_id?: string;
|
|
154
|
+
/** agent_kind / os / lane / wiki_version / matched_install — see onboarding-context.ts */
|
|
155
|
+
context?: Record<string, string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** data of POST /v1/agents/connect/check. Older backends omit `bound_agent`. */
|
|
159
|
+
export interface AgentConnectCheckResult {
|
|
160
|
+
pairable: boolean;
|
|
161
|
+
status: string;
|
|
162
|
+
expires_at?: string;
|
|
163
|
+
user_id_status?: string;
|
|
164
|
+
bound_agent?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
export type AgentAppView = { id: string; app_id?: string; name: string; url: string };
|
|
150
168
|
|
|
151
169
|
export type ClawlingApiErrorKind =
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ClawlingApiError } from "./api-types.ts";
|
|
2
|
+
import type { OpenclawClawlingApiClient } from "./api-client.ts";
|
|
3
|
+
import { AGENTS_CONNECT_PLATFORM, RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
|
|
4
|
+
|
|
5
|
+
export interface ConnectPreCheck {
|
|
6
|
+
pairable: boolean;
|
|
7
|
+
status: string;
|
|
8
|
+
boundAgent: boolean;
|
|
9
|
+
userIdStatus: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Non-consuming pre-check of a connect code. Records the "checked" funnel
|
|
14
|
+
* stage server-side and tells us whether the code is bound to an existing
|
|
15
|
+
* agent (the reconnect prompt). Any transport-level failure — older backend,
|
|
16
|
+
* rate limit, network — degrades to `null`: the pre-check is telemetry plus a
|
|
17
|
+
* courtesy, never a gate in front of `/connect`.
|
|
18
|
+
*/
|
|
19
|
+
export async function preCheckConnectCode(
|
|
20
|
+
client: Pick<OpenclawClawlingApiClient, "agentsConnectCheck">,
|
|
21
|
+
input: { code: string; userId?: string; context?: Record<string, string> },
|
|
22
|
+
log: (message: string) => void,
|
|
23
|
+
): Promise<ConnectPreCheck | null> {
|
|
24
|
+
try {
|
|
25
|
+
const res = await client.agentsConnectCheck({
|
|
26
|
+
code: input.code,
|
|
27
|
+
platform: AGENTS_CONNECT_PLATFORM,
|
|
28
|
+
...(input.userId ? { user_id: input.userId } : {}),
|
|
29
|
+
...(input.context ? { context: input.context } : {}),
|
|
30
|
+
});
|
|
31
|
+
// A 200 with no data says nothing about the code: degrade exactly like an
|
|
32
|
+
// unreachable endpoint (Hermes' `evaluate_precheck({})` does the same).
|
|
33
|
+
if (!res || typeof res !== "object" || Object.keys(res).length === 0) {
|
|
34
|
+
log("Connect-code pre-check returned no data; continuing without it.");
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
pairable: res.pairable === true,
|
|
39
|
+
status: typeof res.status === "string" ? res.status : "",
|
|
40
|
+
boundAgent: res.bound_agent === true,
|
|
41
|
+
userIdStatus: typeof res.user_id_status === "string" ? res.user_id_status : "",
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const kind = err instanceof ClawlingApiError ? err.kind : "error";
|
|
45
|
+
log(`Connect-code pre-check unavailable (${kind}); continuing without it.`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Owner-facing explanation for `pairable: false`. No flags, no minutes, one URL. */
|
|
51
|
+
export function unpairableMessage(pre: ConnectPreCheck): string {
|
|
52
|
+
if (pre.userIdStatus === "owner_mismatch" && pre.boundAgent) {
|
|
53
|
+
// The server cannot tell us whether the bound agent shares this identity's
|
|
54
|
+
// owner, only that it is a different agent — so say exactly that.
|
|
55
|
+
return (
|
|
56
|
+
"This connect code is the reconnect prompt for a different agent than the identity stored here. " +
|
|
57
|
+
"Ask your owner to send the reconnect prompt from THIS agent's chat in the ClawChat app."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (pre.userIdStatus === "invalid") {
|
|
61
|
+
return (
|
|
62
|
+
"The identity stored here is not a valid ClawChat user id, so it cannot be restored. " +
|
|
63
|
+
"Activate as a brand-new agent with --new-account."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (pre.userIdStatus === "owner_mismatch") {
|
|
67
|
+
return (
|
|
68
|
+
"This connect code belongs to a different ClawChat account than the identity stored here. " +
|
|
69
|
+
"Ask the owner of THIS agent for a code, or activate as a brand-new agent with --new-account."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
switch (pre.status) {
|
|
73
|
+
case "paired":
|
|
74
|
+
return (
|
|
75
|
+
"This connect code was already redeemed. If this agent lost its connection, ask your owner " +
|
|
76
|
+
`to send you the reconnect prompt from the ClawChat app and follow ${RECONNECT_GUIDE_URL}; ` +
|
|
77
|
+
"otherwise ask for a fresh code."
|
|
78
|
+
);
|
|
79
|
+
case "expired":
|
|
80
|
+
case "invalid":
|
|
81
|
+
return `This connect code is ${pre.status}. Ask your owner for a fresh code from the ClawChat app.`;
|
|
82
|
+
default:
|
|
83
|
+
return `This connect code is not pairable (status=${pre.status || "unknown"}). Ask your owner for a fresh code from the ClawChat app.`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Refusal for a new-identity intent (`--new-account` or the interactive
|
|
89
|
+
* choice) on a bound code: without a user_id, `/connect` restores the bound
|
|
90
|
+
* agent instead of creating one. No flags, no minutes, no URL.
|
|
91
|
+
*/
|
|
92
|
+
export function boundCodeNewIdentityMessage(): string {
|
|
93
|
+
return (
|
|
94
|
+
"This is a reconnect code bound to an existing agent, so it cannot create a new agent. " +
|
|
95
|
+
"Ask your owner for a normal connect code from the ClawChat app, or use this reconnect prompt " +
|
|
96
|
+
"on the agent it belongs to."
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/login.runtime.ts
CHANGED
|
@@ -14,24 +14,16 @@ import {
|
|
|
14
14
|
resolveOpenclawClawlingAccount,
|
|
15
15
|
} from "./config.ts";
|
|
16
16
|
import { getClawChatStore, type ClawChatStore } from "./storage.ts";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export const AGENTS_CONNECT_TYPE = "clawbot" as const;
|
|
28
|
-
/**
|
|
29
|
-
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
30
|
-
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
31
|
-
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
32
|
-
* retries once.
|
|
33
|
-
*/
|
|
34
|
-
export const AGENT_NOT_FOUND_CODE = 16001 as const;
|
|
17
|
+
import { buildOnboardingContext, RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
|
|
18
|
+
import { boundCodeNewIdentityMessage, preCheckConnectCode, unpairableMessage } from "./connect-check.ts";
|
|
19
|
+
// Re-exported: `src/login.runtime.test.ts` and `src/commands.ts` import these
|
|
20
|
+
// from here, so the historical import path keeps working after the move.
|
|
21
|
+
import {
|
|
22
|
+
AGENTS_CONNECT_PLATFORM,
|
|
23
|
+
AGENTS_CONNECT_TYPE,
|
|
24
|
+
AGENT_NOT_FOUND_CODE,
|
|
25
|
+
} from "./onboarding-context.ts";
|
|
26
|
+
export { AGENTS_CONNECT_PLATFORM, AGENTS_CONNECT_TYPE, AGENT_NOT_FOUND_CODE };
|
|
35
27
|
|
|
36
28
|
export type OpenclawClawchatMutateConfigFile = <T = void>(params: {
|
|
37
29
|
afterWrite: { mode: "auto" } | { mode: "none" | "restart"; reason: string };
|
|
@@ -103,9 +95,11 @@ export class ExistingActivationError extends Error {
|
|
|
103
95
|
"spent. Re-run stating the intent:\n" +
|
|
104
96
|
" - Pair as a BRAND-NEW agent (this instance stops using the identity " +
|
|
105
97
|
"above): /clawchat-activate <CODE> --new-account\n" +
|
|
106
|
-
" - RESTORE the identity above
|
|
107
|
-
"
|
|
108
|
-
"
|
|
98
|
+
" - RESTORE the identity above: do not spend a fresh code on it. Ask your " +
|
|
99
|
+
"owner to send you the reconnect prompt from the ClawChat app — its code is " +
|
|
100
|
+
"bound to this identity and usually restores it on its own; if activation " +
|
|
101
|
+
"still reports it as already paired, run it again with --repair " +
|
|
102
|
+
`(/clawchat-activate <CODE> --repair) — and follow ${RECONNECT_GUIDE_URL}\n` +
|
|
109
103
|
" - To run a SECOND agent alongside this one instead, activate it as a " +
|
|
110
104
|
"named account: /clawchat-activate <CODE> --account <name> (or: openclaw " +
|
|
111
105
|
"channels add --channel clawchat-plugin-openclaw --account <name> --token <CODE>)",
|
|
@@ -289,7 +283,63 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
289
283
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
290
284
|
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
291
285
|
|
|
292
|
-
|
|
286
|
+
const inviteCode = (
|
|
287
|
+
await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()
|
|
288
|
+
).trim();
|
|
289
|
+
if (!inviteCode) {
|
|
290
|
+
throw new Error("Login aborted: invite code is required.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
|
|
294
|
+
baseUrl: account.baseUrl,
|
|
295
|
+
mediaBaseUrl: account.mediaBaseUrl,
|
|
296
|
+
// Pre-login we may not have a token yet. Send the current one (or empty)
|
|
297
|
+
// — the server should accept an unauthenticated invite-code exchange.
|
|
298
|
+
token: account.token || "",
|
|
299
|
+
pluginVersion: resolvePluginVersion(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Telemetry the backend joins with wiki field reports; ignored by older servers.
|
|
303
|
+
const onboarding = buildOnboardingContext();
|
|
304
|
+
|
|
305
|
+
// Non-consuming pre-check. `null` = endpoint unavailable, carry on. A bound
|
|
306
|
+
// code (the owner's reconnect prompt) is the owner's own proof of intent, so
|
|
307
|
+
// it settles the "new agent or restore?" question below without a flag.
|
|
308
|
+
//
|
|
309
|
+
// The pre-check only carries the `user_id` that `/connect` would replay. An
|
|
310
|
+
// explicit `newAccount` never replays it, so it must not be judged against
|
|
311
|
+
// it either: the server marks `pairable:false` for an `owner_mismatch` /
|
|
312
|
+
// `invalid` id, and that would block the very escape the refusal recommends.
|
|
313
|
+
const storedUserId = account.userId.trim();
|
|
314
|
+
const precheckUserId = params.newAccount ? "" : storedUserId;
|
|
315
|
+
runtime.log("Checking the invite code …");
|
|
316
|
+
let precheck = await preCheckConnectCode(
|
|
317
|
+
apiClient,
|
|
318
|
+
{ code: inviteCode, userId: precheckUserId || undefined, context: onboarding },
|
|
319
|
+
runtime.log,
|
|
320
|
+
);
|
|
321
|
+
// Whether the operator will be asked "new agent or restore?" below. When
|
|
322
|
+
// they will, a refusal caused only by the stored identity (not by the code)
|
|
323
|
+
// is deferred: choosing a brand-new identity drops that id, so the verdict
|
|
324
|
+
// on it no longer applies.
|
|
325
|
+
const identityUnsettled =
|
|
326
|
+
storedUserId !== "" && account.token.trim() !== "" && !params.newAccount && !params.repair;
|
|
327
|
+
const refusedForIdentity =
|
|
328
|
+
precheck !== null &&
|
|
329
|
+
!precheck.pairable &&
|
|
330
|
+
precheckUserId !== "" &&
|
|
331
|
+
// Only a live code: a dead one (expired / paired / invalid) refuses on its
|
|
332
|
+
// own account whatever identity is chosen, so there is nothing to defer.
|
|
333
|
+
precheck.status === "pending" &&
|
|
334
|
+
(precheck.userIdStatus === "owner_mismatch" || precheck.userIdStatus === "invalid");
|
|
335
|
+
const deferredRefusal = identityUnsettled && refusedForIdentity ? precheck : null;
|
|
336
|
+
if (precheck && !precheck.pairable && !deferredRefusal) {
|
|
337
|
+
throw new Error(unpairableMessage(precheck));
|
|
338
|
+
}
|
|
339
|
+
const boundToIncumbent =
|
|
340
|
+
precheck?.pairable === true && precheck.boundAgent === true && precheckUserId !== "";
|
|
341
|
+
|
|
342
|
+
// Fork before the invite code is spent, so neither branch spends the code
|
|
293
343
|
// before the outcome is settled. "Live" is decided by whether a usable token
|
|
294
344
|
// remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
|
|
295
345
|
// logged-out instance still re-pairs with no flag at all, which is the flow
|
|
@@ -299,50 +349,60 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
299
349
|
// ambiguous, because redeeming a code here can either mint a new agent or
|
|
300
350
|
// re-pair the incumbent one. Ask which, and only refuse when nobody answers.
|
|
301
351
|
// An explicit `newAccount` / `repair` from the caller has already settled it,
|
|
302
|
-
// so no prompt.
|
|
352
|
+
// so no prompt. Nor is one needed when the pre-check proved the code is the
|
|
353
|
+
// owner's own reconnect prompt for this very agent (`boundToIncumbent`) — a
|
|
354
|
+
// bound code can only ever restore the incumbent identity.
|
|
303
355
|
let newAccount = params.newAccount;
|
|
304
|
-
if (
|
|
356
|
+
if (identityUnsettled && !boundToIncumbent) {
|
|
305
357
|
const identity = account.agentId.trim()
|
|
306
|
-
? `agent ${account.agentId.trim()} (shadow user ${
|
|
307
|
-
: `agent ${
|
|
358
|
+
? `agent ${account.agentId.trim()} (shadow user ${storedUserId})`
|
|
359
|
+
: `agent ${storedUserId}`;
|
|
308
360
|
const intent = await (params.readActivationIntent ??
|
|
309
361
|
(() => promptActivationIntentFromStdin(runtime, identity)))();
|
|
310
362
|
// Only "new-account" changes what gets sent. Restoring needs no flag:
|
|
311
363
|
// replaying the stored `user_id` IS the re-pair, and that is already the
|
|
312
364
|
// default below — so choosing it simply means "don't refuse".
|
|
313
|
-
if (intent === "new-account")
|
|
314
|
-
|
|
315
|
-
|
|
365
|
+
if (intent === "new-account") {
|
|
366
|
+
newAccount = true;
|
|
367
|
+
if (deferredRefusal) {
|
|
368
|
+
// The first verdict judged the id we are now dropping; re-check the
|
|
369
|
+
// code on its own before spending it.
|
|
370
|
+
precheck = await preCheckConnectCode(
|
|
371
|
+
apiClient,
|
|
372
|
+
{ code: inviteCode, context: onboarding },
|
|
373
|
+
runtime.log,
|
|
374
|
+
);
|
|
375
|
+
if (precheck && !precheck.pairable) {
|
|
376
|
+
throw new Error(unpairableMessage(precheck));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else if (deferredRefusal) {
|
|
380
|
+
// Restoring (or nobody to ask) replays the id the server already refused.
|
|
381
|
+
throw new Error(unpairableMessage(deferredRefusal));
|
|
382
|
+
} else if (intent === null) {
|
|
383
|
+
throw new ExistingActivationError(storedUserId, account.agentId.trim());
|
|
316
384
|
}
|
|
317
385
|
}
|
|
318
386
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
throw new Error(
|
|
387
|
+
// A new identity (flag or interactive choice) sends no user_id, and /connect
|
|
388
|
+
// without one on a bound code silently RESTORES the bound agent and spends
|
|
389
|
+
// the code. A reconnect code can never mint an agent, so refuse first.
|
|
390
|
+
if (newAccount && precheck?.boundAgent === true) {
|
|
391
|
+
throw new Error(boundCodeNewIdentityMessage());
|
|
324
392
|
}
|
|
325
393
|
|
|
326
|
-
const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
|
|
327
|
-
baseUrl: account.baseUrl,
|
|
328
|
-
mediaBaseUrl: account.mediaBaseUrl,
|
|
329
|
-
// Pre-login we may not have a token yet. Send the current one (or empty)
|
|
330
|
-
// — the server should accept an unauthenticated invite-code exchange.
|
|
331
|
-
token: account.token || "",
|
|
332
|
-
pluginVersion: resolvePluginVersion(),
|
|
333
|
-
});
|
|
334
|
-
|
|
335
394
|
runtime.log("Verifying invite code …");
|
|
336
395
|
let result;
|
|
337
396
|
// A brand-new identity is precisely "do not replay" — the replay is the only
|
|
338
397
|
// thing that would bind this code to the incumbent agent.
|
|
339
|
-
const existingUserId = newAccount ? "" :
|
|
398
|
+
const existingUserId = newAccount ? "" : storedUserId;
|
|
340
399
|
try {
|
|
341
400
|
result = await apiClient.agentsConnect({
|
|
342
401
|
code: inviteCode,
|
|
343
402
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
344
403
|
type: AGENTS_CONNECT_TYPE,
|
|
345
404
|
...(existingUserId ? { user_id: existingUserId } : {}),
|
|
405
|
+
context: onboarding,
|
|
346
406
|
});
|
|
347
407
|
} catch (err) {
|
|
348
408
|
if (err instanceof ClawlingApiError) {
|
|
@@ -363,6 +423,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
363
423
|
code: inviteCode,
|
|
364
424
|
platform: AGENTS_CONNECT_PLATFORM,
|
|
365
425
|
type: AGENTS_CONNECT_TYPE,
|
|
426
|
+
context: onboarding,
|
|
366
427
|
});
|
|
367
428
|
} catch (retryErr) {
|
|
368
429
|
if (retryErr instanceof ClawlingApiError) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding telemetry the plugin attaches to `/v1/agents/connect` and
|
|
3
|
+
* `/v1/agents/connect/check` (all optional, all ignored by older backends).
|
|
4
|
+
*
|
|
5
|
+
* Values mirror the connection wiki's manifest vocabulary so the backend can
|
|
6
|
+
* join plugin activations with wiki field reports without an alias table:
|
|
7
|
+
* - `agent_kind` = the wiki `match.agent` name for this host
|
|
8
|
+
* - `lane` = "self": this plugin holds its own WebSocket + token
|
|
9
|
+
* - `matched_install` = the wiki page slug that routes to this plugin
|
|
10
|
+
* Nothing here is a credential and nothing here gates activation.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const AGENT_KIND = "openclaw" as const;
|
|
14
|
+
export const LANE = "self" as const;
|
|
15
|
+
export const MATCHED_INSTALL = "install/official-openclaw" as const;
|
|
16
|
+
|
|
17
|
+
/** Public connection wiki. `/start.md` is the entry page; `/reconnect.md` the re-pair page. */
|
|
18
|
+
export const START_GUIDE_URL = "https://agent-connection.clawling.com/start.md" as const;
|
|
19
|
+
export const RECONNECT_GUIDE_URL = "https://agent-connection.clawling.com/reconnect.md" as const;
|
|
20
|
+
|
|
21
|
+
const WIKI_VERSION_MAX = 40;
|
|
22
|
+
|
|
23
|
+
/** Wiki `os` enum (windows | macos | linux); "" for anything else so callers omit it. */
|
|
24
|
+
export function hostOs(plat: string = process.platform): "windows" | "macos" | "linux" | "" {
|
|
25
|
+
switch (plat) {
|
|
26
|
+
case "win32":
|
|
27
|
+
return "windows";
|
|
28
|
+
case "darwin":
|
|
29
|
+
return "macos";
|
|
30
|
+
case "linux":
|
|
31
|
+
return "linux";
|
|
32
|
+
default:
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The wiki build stamp the agent saw on the install page. The official install
|
|
39
|
+
* page tells the agent to export `CLAWCHAT_WIKI_VERSION` before activating; a
|
|
40
|
+
* hand-run `openclaw channels add` simply has none.
|
|
41
|
+
*/
|
|
42
|
+
export function wikiVersion(env: NodeJS.ProcessEnv = process.env): string {
|
|
43
|
+
return (env.CLAWCHAT_WIKI_VERSION ?? "").trim().slice(0, WIKI_VERSION_MAX);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildOnboardingContext(
|
|
47
|
+
opts: { platform?: string; env?: NodeJS.ProcessEnv } = {},
|
|
48
|
+
): Record<string, string> {
|
|
49
|
+
const out: Record<string, string> = {
|
|
50
|
+
agent_kind: AGENT_KIND,
|
|
51
|
+
lane: LANE,
|
|
52
|
+
matched_install: MATCHED_INSTALL,
|
|
53
|
+
};
|
|
54
|
+
const os = hostOs(opts.platform);
|
|
55
|
+
if (os) out.os = os;
|
|
56
|
+
const wiki = wikiVersion(opts.env);
|
|
57
|
+
if (wiki) out.wiki_version = wiki;
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
63
|
+
* agent runtime — openclaw's bundled clawchat channel.
|
|
64
|
+
*/
|
|
65
|
+
export const AGENTS_CONNECT_PLATFORM = "openclaw" as const;
|
|
66
|
+
/**
|
|
67
|
+
* Agent type tag sent to `/v1/agents/connect`. The clawchat channel is
|
|
68
|
+
* always a bot; humans don't log in through this flow.
|
|
69
|
+
*/
|
|
70
|
+
export const AGENTS_CONNECT_TYPE = "clawbot" as const;
|
|
71
|
+
/**
|
|
72
|
+
* `/v1/agents/connect` envelope code for "the supplied user_id matches no
|
|
73
|
+
* agent". Servers that predate the server-side stale-user_id fallback return
|
|
74
|
+
* this instead of degrading to a fresh pairing, so the client sheds the id and
|
|
75
|
+
* retries once.
|
|
76
|
+
*/
|
|
77
|
+
export const AGENT_NOT_FOUND_CODE = 16001 as const;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optional onboarding facts the AGENT writes after following the connection
|
|
7
|
+
* wiki: the id of the field report it filed there, and its self-assessed
|
|
8
|
+
* capability tier. The plugin only relays them on the plugin-report call; it
|
|
9
|
+
* never files a wiki report itself. Same directory as the greeting override
|
|
10
|
+
* (`~/clawchat/`), so the agent already knows the place.
|
|
11
|
+
*/
|
|
12
|
+
export interface OnboardingReportFields {
|
|
13
|
+
wiki_report_id?: string;
|
|
14
|
+
capability_tier?: number;
|
|
15
|
+
capability_ceiling?: number;
|
|
16
|
+
capabilities?: { headless?: boolean; mcp?: boolean; permission_hook?: boolean; session_line?: boolean };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const REPORT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
20
|
+
const CAP_KEYS = ["headless", "mcp", "permission_hook", "session_line"] as const;
|
|
21
|
+
|
|
22
|
+
function tier(v: unknown): number | undefined {
|
|
23
|
+
return Number.isInteger(v) && (v as number) >= 1 && (v as number) <= 4 ? (v as number) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read + validate `~/clawchat/onboarding.json`. Never throws: an unresolvable
|
|
28
|
+
* home directory, an absent/unreadable file, or malformed JSON all fall
|
|
29
|
+
* through to `null`, same as a file with no valid fields — this must be safe
|
|
30
|
+
* to call unconditionally from a best-effort report path.
|
|
31
|
+
*/
|
|
32
|
+
export function readOnboardingReport(homeDir?: string): OnboardingReportFields | null {
|
|
33
|
+
try {
|
|
34
|
+
const base = homeDir ?? os.homedir();
|
|
35
|
+
const file = path.join(base, "clawchat", "onboarding.json");
|
|
36
|
+
const raw: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
37
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
38
|
+
const o = raw as Record<string, unknown>;
|
|
39
|
+
const out: OnboardingReportFields = {};
|
|
40
|
+
if (typeof o.wiki_report_id === "string" && REPORT_ID_RE.test(o.wiki_report_id)) out.wiki_report_id = o.wiki_report_id;
|
|
41
|
+
const t = tier(o.capability_tier);
|
|
42
|
+
if (t !== undefined) out.capability_tier = t;
|
|
43
|
+
const c = tier(o.capability_ceiling);
|
|
44
|
+
// The backend rejects the WHOLE report (22004) when ceiling < tier, which
|
|
45
|
+
// would silently stop the version row updating. Keep the tier, drop the
|
|
46
|
+
// inconsistent ceiling.
|
|
47
|
+
if (c !== undefined && (t === undefined || c >= t)) out.capability_ceiling = c;
|
|
48
|
+
if (o.capabilities && typeof o.capabilities === "object") {
|
|
49
|
+
const caps: NonNullable<OnboardingReportFields["capabilities"]> = {};
|
|
50
|
+
for (const k of CAP_KEYS) {
|
|
51
|
+
const v = (o.capabilities as Record<string, unknown>)[k];
|
|
52
|
+
if (typeof v === "boolean") caps[k] = v;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(caps).length > 0) out.capabilities = caps;
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/plugin-report.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
5
|
+
import type { OnboardingReportFields } from "./onboarding-report.ts";
|
|
5
6
|
|
|
6
7
|
const PACKAGE_NAME = "@clawling/clawchat-plugin-openclaw";
|
|
7
8
|
|
|
@@ -48,6 +49,8 @@ export interface ReportParams {
|
|
|
48
49
|
pluginVersion: string;
|
|
49
50
|
agentVersion: string;
|
|
50
51
|
authenticated: boolean;
|
|
52
|
+
/** Agent-written onboarding facts (`~/clawchat/onboarding.json`); optional. */
|
|
53
|
+
onboarding?: OnboardingReportFields | null;
|
|
51
54
|
log?: { debug?: (msg: string) => void };
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -70,6 +73,7 @@ export async function reportPluginVersionSafe(p: ReportParams): Promise<void> {
|
|
|
70
73
|
agentVersion: p.agentVersion,
|
|
71
74
|
runtimeName: "node",
|
|
72
75
|
runtimeVersion: process.version,
|
|
76
|
+
onboarding: p.onboarding ?? null,
|
|
73
77
|
},
|
|
74
78
|
{ authenticated: p.authenticated },
|
|
75
79
|
);
|
package/src/runtime.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
|
19
19
|
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
20
20
|
import { buildFriendGreetingEnvelope } from "./friend-greeting.ts";
|
|
21
21
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
22
|
+
import { readOnboardingReport } from "./onboarding-report.ts";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import {
|
|
24
25
|
ensureLivewareCli,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./liveware-sample.ts";
|
|
34
35
|
import { ClawlingApiError } from "./api-types.ts";
|
|
35
36
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
37
|
+
import { RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
|
|
36
38
|
import {
|
|
37
39
|
runOpenclawClawlingLogin,
|
|
38
40
|
type LoginParams,
|
|
@@ -210,9 +212,13 @@ const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
|
|
|
210
212
|
]);
|
|
211
213
|
const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
|
|
212
214
|
// §C.1 — user-visible message emitted on permanent token expiry. Kept
|
|
213
|
-
// byte-identical to the Hermes plugin (parity spec §C.1.4).
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
// byte-identical to the Hermes plugin (parity spec §C.1.4). Points the owner
|
|
216
|
+
// at the reconnect prompt (a code bound to this identity) rather than at a
|
|
217
|
+
// slash command: a fresh create code spent here would mint a second agent.
|
|
218
|
+
export const CLAWCHAT_TOKEN_EXPIRED_MESSAGE =
|
|
219
|
+
"ClawChat token expired and could not be refreshed. Ask your owner to send you the reconnect prompt from the ClawChat app, then follow " +
|
|
220
|
+
RECONNECT_GUIDE_URL +
|
|
221
|
+
".";
|
|
216
222
|
const CLAWCHAT_TOKEN_EXPIRED_LAST_ERROR = "token expired — re-pair required";
|
|
217
223
|
|
|
218
224
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -1067,6 +1073,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1067
1073
|
pluginVersion,
|
|
1068
1074
|
agentVersion,
|
|
1069
1075
|
authenticated: false,
|
|
1076
|
+
onboarding: readOnboardingReport(),
|
|
1070
1077
|
log,
|
|
1071
1078
|
});
|
|
1072
1079
|
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
@@ -1139,6 +1146,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1139
1146
|
pluginVersion,
|
|
1140
1147
|
agentVersion,
|
|
1141
1148
|
authenticated: true,
|
|
1149
|
+
onboarding: readOnboardingReport(),
|
|
1142
1150
|
log,
|
|
1143
1151
|
});
|
|
1144
1152
|
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
package/src/skill-update.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const OFFICIAL_SKILLS_BASE =
|
|
|
71
71
|
* in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
|
|
72
72
|
* imports the same ref, so the `livewares` tree at that tag is pinned too.
|
|
73
73
|
*/
|
|
74
|
-
export const DEFAULT_SKILLS_REF = "skills-v1.
|
|
74
|
+
export const DEFAULT_SKILLS_REF = "skills-v1.11.0";
|
|
75
75
|
|
|
76
76
|
/** Refuse to treat an absurdly large response as a skill file (defence in depth). */
|
|
77
77
|
export const MAX_SKILL_BYTES = 256 * 1024;
|