@clawling/clawchat-plugin-openclaw 2026.9.14-2 → 2026.9.17-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 +84 -9
- package/dist/src/config.js +3 -0
- package/dist/src/connect-check.js +74 -0
- package/dist/src/friend-greeting.js +80 -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/protocol-types.js +1 -0
- package/dist/src/refresh-manager.js +77 -10
- package/dist/src/runtime.js +117 -2
- package/dist/src/skill-update.js +1 -1
- package/dist/src/tools-schema.js +6 -0
- package/dist/src/tools.js +32 -1
- package/dist/src/ws-client.js +5 -0
- package/openclaw.plugin.json +5 -0
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +32 -3
- package/skills/clawchat-set-greeting/SKILL.md +23 -4
- package/skills/manifest.json +12 -12
- package/src/api-client.ts +116 -23
- package/src/api-types.ts +18 -0
- package/src/config.ts +7 -0
- package/src/connect-check.ts +98 -0
- package/src/friend-greeting.ts +91 -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/protocol-types.ts +1 -0
- package/src/refresh-manager.ts +89 -10
- package/src/runtime.ts +135 -3
- package/src/skill-update.ts +1 -1
- package/src/tools-schema.ts +8 -0
- package/src/tools.ts +37 -0
- package/src/ws-client.ts +4 -0
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 {
|
|
@@ -133,6 +158,8 @@ export interface OpenclawClawlingApiClient {
|
|
|
133
158
|
text: string;
|
|
134
159
|
}): Promise<{ comment: MomentComment }>;
|
|
135
160
|
deleteMomentComment(params: { momentId: number; commentId: number }): Promise<{ ok: boolean }>;
|
|
161
|
+
/** Find-or-create the direct conversation with a friend (`POST /v1/conversations/direct`); 19012 when not a friend. */
|
|
162
|
+
getDirectConversation(peerId: string): Promise<{ conversation: { id: string; type: string } }>;
|
|
136
163
|
getConversation(conversationId: string): Promise<{ conversation: ConversationDetails }>;
|
|
137
164
|
leaveConversation(conversationId: string): Promise<{ ok: boolean }>;
|
|
138
165
|
addConversationMember(
|
|
@@ -147,19 +174,16 @@ export interface OpenclawClawlingApiClient {
|
|
|
147
174
|
uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
|
|
148
175
|
/**
|
|
149
176
|
* Exchange an invite code for an agent token.
|
|
150
|
-
* Request body shape: `{ code, platform, type, user_id?, plugin_version? }`.
|
|
151
|
-
* `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`).
|
|
152
180
|
*/
|
|
153
|
-
agentsConnect(params:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
type: string;
|
|
160
|
-
/** Existing configured ClawChat user id, when re-activating an account. */
|
|
161
|
-
user_id?: string;
|
|
162
|
-
}): Promise<AgentConnectResult>;
|
|
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.
|
|
185
|
+
*/
|
|
186
|
+
agentsConnectCheck(input: AgentConnectCheckInput): Promise<AgentConnectCheckResult>;
|
|
163
187
|
/**
|
|
164
188
|
* Upload an avatar image via `POST /v1/files/upload-url`. The resulting
|
|
165
189
|
* `url` is what you then pass to `updateMyProfile({ avatar_url: url })`.
|
|
@@ -211,14 +235,25 @@ const CODE_INTERNAL = 1; // CodeInternal — transient.
|
|
|
211
235
|
const CODE_BAD_REQUEST = 400; // bad body / device id — permanent (client bug).
|
|
212
236
|
/**
|
|
213
237
|
* CodeInvalidRefresh — returned for BOTH a genuinely revoked/invalid refresh
|
|
214
|
-
* token AND a
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* `
|
|
218
|
-
*
|
|
238
|
+
* token AND a refresh token that was already CONSUMED by a prior successful
|
|
239
|
+
* rotation (a duplicate-supervisor / concurrent-refresh / stale-store race), once
|
|
240
|
+
* the backend's short replay grace window no longer covers it. The stateless
|
|
241
|
+
* `authRefresh` cannot tell the two apart, so it reports `permanent`; the
|
|
242
|
+
* stateful `RefreshManager` re-classifies a consumed-rotation race back to
|
|
243
|
+
* transient (see §B race) before any auto-logout.
|
|
219
244
|
*/
|
|
220
245
|
export const CODE_INVALID_REFRESH = 10003;
|
|
221
246
|
|
|
247
|
+
/**
|
|
248
|
+
* §B attempt deadline — total wall clock for one refresh attempt (request AND
|
|
249
|
+
* response body). A hit aborts the request and is TRANSIENT. Kept below
|
|
250
|
+
* `MIN_REFRESH_INTERVAL_MS` (30s) so the min-interval floor, not the deadline,
|
|
251
|
+
* sets the retry cadence: a retry of the same refresh token then lands inside
|
|
252
|
+
* the backend refresh grace window if the server rotated but the response was
|
|
253
|
+
* lost.
|
|
254
|
+
*/
|
|
255
|
+
export const REFRESH_REQUEST_TIMEOUT_MS = 20_000;
|
|
256
|
+
|
|
222
257
|
/**
|
|
223
258
|
* §0/§B — call `POST /v1/auth/refresh` to rotate the access+refresh token.
|
|
224
259
|
*
|
|
@@ -228,9 +263,14 @@ export const CODE_INVALID_REFRESH = 10003;
|
|
|
228
263
|
* always HTTP 200 — branch on the envelope `code`, NOT on HTTP status. This is
|
|
229
264
|
* a standalone function (not a method on the token-bearing client) precisely
|
|
230
265
|
* because no bearer token participates.
|
|
266
|
+
*
|
|
267
|
+
* The whole attempt is bounded by `REFRESH_REQUEST_TIMEOUT_MS` (§B attempt
|
|
268
|
+
* deadline): the request is aborted via its signal, and the deadline is also
|
|
269
|
+
* raced against the attempt so a fetch implementation that ignores the signal
|
|
270
|
+
* cannot hang the caller.
|
|
231
271
|
*/
|
|
232
272
|
export async function authRefresh(
|
|
233
|
-
opts: { baseUrl: string; fetchImpl?: typeof fetch },
|
|
273
|
+
opts: { baseUrl: string; fetchImpl?: typeof fetch; timeoutMs?: number },
|
|
234
274
|
params: AuthRefreshParams,
|
|
235
275
|
): Promise<AuthRefreshResult> {
|
|
236
276
|
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
@@ -238,6 +278,31 @@ export async function authRefresh(
|
|
|
238
278
|
if (!params.refreshToken?.trim()) {
|
|
239
279
|
return { kind: "permanent", code: CODE_INVALID_REFRESH, message: "missing refresh token" };
|
|
240
280
|
}
|
|
281
|
+
const timeoutMs = opts.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS;
|
|
282
|
+
const controller = new AbortController();
|
|
283
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
284
|
+
const deadline = new Promise<AuthRefreshResult>((resolve) => {
|
|
285
|
+
timer = setTimeout(() => {
|
|
286
|
+
controller.abort(new Error("refresh request timed out"));
|
|
287
|
+
resolve({ kind: "transient", message: `refresh request timed out after ${timeoutMs}ms` });
|
|
288
|
+
}, timeoutMs);
|
|
289
|
+
});
|
|
290
|
+
try {
|
|
291
|
+
return await Promise.race([
|
|
292
|
+
authRefreshAttempt(baseUrl, fetchImpl, params, controller.signal),
|
|
293
|
+
deadline,
|
|
294
|
+
]);
|
|
295
|
+
} finally {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function authRefreshAttempt(
|
|
301
|
+
baseUrl: string,
|
|
302
|
+
fetchImpl: typeof fetch,
|
|
303
|
+
params: AuthRefreshParams,
|
|
304
|
+
signal: AbortSignal,
|
|
305
|
+
): Promise<AuthRefreshResult> {
|
|
241
306
|
let res: Response;
|
|
242
307
|
try {
|
|
243
308
|
res = await fetchImpl(`${baseUrl}/v1/auth/refresh`, {
|
|
@@ -248,6 +313,8 @@ export async function authRefresh(
|
|
|
248
313
|
"x-device-id": params.deviceId,
|
|
249
314
|
},
|
|
250
315
|
body: JSON.stringify({ refresh_token: params.refreshToken.trim() }),
|
|
316
|
+
// Still in effect while the body is read below.
|
|
317
|
+
signal,
|
|
251
318
|
});
|
|
252
319
|
} catch (err) {
|
|
253
320
|
// Network error / timeout / DNS — TRANSIENT (no rotation committed).
|
|
@@ -292,8 +359,9 @@ export async function authRefresh(
|
|
|
292
359
|
const refreshToken = typeof data.refresh_token === "string" ? data.refresh_token : "";
|
|
293
360
|
if (!accessToken || !refreshToken) {
|
|
294
361
|
// Rotation succeeded server-side but the body is malformed — transient so
|
|
295
|
-
// we retry; the
|
|
296
|
-
//
|
|
362
|
+
// we retry; inside the backend grace window the retry redeems the old
|
|
363
|
+
// token again, after it the retry returns 10003 and escalates to
|
|
364
|
+
// permanent (§B transient→permanent).
|
|
297
365
|
return { kind: "transient", status: 200, message: "refresh: rotation body incomplete" };
|
|
298
366
|
}
|
|
299
367
|
return { kind: "success", accessToken, refreshToken };
|
|
@@ -639,6 +707,17 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
639
707
|
`/v1/moments/${encodeURIComponent(String(params.momentId))}/comments/${encodeURIComponent(String(params.commentId))}`,
|
|
640
708
|
);
|
|
641
709
|
},
|
|
710
|
+
async getDirectConversation(peerId): Promise<{ conversation: { id: string; type: string } }> {
|
|
711
|
+
assertNonBlankId(peerId, "getDirectConversation: peerId");
|
|
712
|
+
return await call<{ conversation: { id: string; type: string } }>(
|
|
713
|
+
"POST",
|
|
714
|
+
"/v1/conversations/direct",
|
|
715
|
+
{
|
|
716
|
+
body: JSON.stringify({ peer_id: peerId.trim() }),
|
|
717
|
+
headers: { "content-type": "application/json" },
|
|
718
|
+
},
|
|
719
|
+
);
|
|
720
|
+
},
|
|
642
721
|
async getConversation(conversationId): Promise<{ conversation: ConversationDetails }> {
|
|
643
722
|
return await call<{ conversation: ConversationDetails }>(
|
|
644
723
|
"GET",
|
|
@@ -698,7 +777,20 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
698
777
|
},
|
|
699
778
|
);
|
|
700
779
|
},
|
|
701
|
-
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> {
|
|
702
794
|
if (!inviteCode?.trim()) {
|
|
703
795
|
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
704
796
|
}
|
|
@@ -719,6 +811,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
719
811
|
if (opts.pluginVersion?.trim()) {
|
|
720
812
|
body.plugin_version = opts.pluginVersion.trim();
|
|
721
813
|
}
|
|
814
|
+
for (const [k, v] of Object.entries(context ?? {})) if (v) body[k] = v;
|
|
722
815
|
return await call<AgentConnectResult>("POST", "/v1/agents/connect", {
|
|
723
816
|
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
724
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 =
|
package/src/config.ts
CHANGED
|
@@ -114,6 +114,8 @@ export type OpenclawClawlingAccountConfig = {
|
|
|
114
114
|
richInteractions?: boolean;
|
|
115
115
|
/** Emit ONE consolidated awareness note to the agent when friend/conversation signals arrive. */
|
|
116
116
|
awarenessNote?: boolean;
|
|
117
|
+
/** Speak first to a newly added non-owner friend (default true). */
|
|
118
|
+
friendGreeting?: boolean;
|
|
117
119
|
/** Auto-install the Liveware Sample demo app when no liveware app is registered (default true). */
|
|
118
120
|
livewareSample?: boolean;
|
|
119
121
|
reconnect?: OpenclawClawlingReconnectConfig;
|
|
@@ -174,6 +176,7 @@ export const openclawClawlingAccountConfigSchema = {
|
|
|
174
176
|
forwardToolCalls: { type: "boolean" },
|
|
175
177
|
richInteractions: { type: "boolean" },
|
|
176
178
|
awarenessNote: { type: "boolean" },
|
|
179
|
+
friendGreeting: { type: "boolean" },
|
|
177
180
|
livewareSample: { type: "boolean" },
|
|
178
181
|
reconnect: {
|
|
179
182
|
type: "object",
|
|
@@ -311,6 +314,7 @@ export type ResolvedOpenclawClawlingAccount = {
|
|
|
311
314
|
forwardToolCalls: boolean;
|
|
312
315
|
richInteractions: boolean;
|
|
313
316
|
awarenessNote: boolean;
|
|
317
|
+
friendGreeting: boolean;
|
|
314
318
|
livewareSample: boolean;
|
|
315
319
|
allowFrom: string[];
|
|
316
320
|
reconnect: Required<OpenclawClawlingReconnectConfig>;
|
|
@@ -649,6 +653,8 @@ export function resolveOpenclawClawlingAccount(
|
|
|
649
653
|
typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
650
654
|
const awarenessNote =
|
|
651
655
|
typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
|
|
656
|
+
const friendGreeting =
|
|
657
|
+
typeof channel.friendGreeting === "boolean" ? channel.friendGreeting : true;
|
|
652
658
|
const livewareSample =
|
|
653
659
|
typeof channel.livewareSample === "boolean" ? channel.livewareSample : true;
|
|
654
660
|
|
|
@@ -678,6 +684,7 @@ export function resolveOpenclawClawlingAccount(
|
|
|
678
684
|
forwardToolCalls,
|
|
679
685
|
richInteractions,
|
|
680
686
|
awarenessNote,
|
|
687
|
+
friendGreeting,
|
|
681
688
|
livewareSample,
|
|
682
689
|
allowFrom: [],
|
|
683
690
|
reconnect: readReconnect(channel.reconnect),
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First message to a newly added NON-owner friend.
|
|
3
|
+
*
|
|
4
|
+
* `friend.added` used to be a pure awareness event. The server creates the
|
|
5
|
+
* direct conversation inside the friend-accept transaction, but the signal
|
|
6
|
+
* only carries the counterparty `usr_…` — so the runtime resolves the
|
|
7
|
+
* conversation through `POST /v1/conversations/direct` and then feeds ONE
|
|
8
|
+
* synthetic inbound turn (built here) into the normal dispatch path, the same
|
|
9
|
+
* way the activation bootstrap greets the owner.
|
|
10
|
+
*
|
|
11
|
+
* The prompt is deliberately distinct from the owner activation prompt: this
|
|
12
|
+
* reader is a stranger, so "you are connected and ready" makes no sense and
|
|
13
|
+
* the agent must say whose agent it is instead.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { EVENT, type Envelope } from "./protocol-types.ts";
|
|
19
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
20
|
+
|
|
21
|
+
export const FRIEND_GREETING_FALLBACK = [
|
|
22
|
+
"A ClawChat user has just become your friend. You are now in a direct conversation with them; they are not your owner.",
|
|
23
|
+
"Reply now with one short, friendly greeting message in this conversation: introduce yourself by name, say you are an AI agent acting on behalf of your owner, and invite them to tell you what they need.",
|
|
24
|
+
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
25
|
+
"Do not share your owner's private information, and do not ask the user for personal information.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
29
|
+
// friend without a restart. Any read failure falls back to the built-in text.
|
|
30
|
+
// Mirrors `buildActivationBootstrapText` (`~/clawchat/greeting.md`).
|
|
31
|
+
export function buildFriendGreetingText(homeDir: string = os.homedir()): string {
|
|
32
|
+
const greetingPath = path.join(homeDir, "clawchat", "friend-greeting.md");
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(greetingPath);
|
|
35
|
+
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
36
|
+
if (override.length > 0) {
|
|
37
|
+
return override;
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
41
|
+
if (code !== "ENOENT") {
|
|
42
|
+
console.warn(`clawchat.friend-greeting failed to read override ${greetingPath}:`, error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return FRIEND_GREETING_FALLBACK;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BuildFriendGreetingEnvelopeParams {
|
|
49
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
50
|
+
/** The resolved direct conversation (`cnv_…`) shared with the new friend. */
|
|
51
|
+
conversationId: string;
|
|
52
|
+
/** The new friend's `usr_…` id — becomes the sender so the turn's session and sender metadata resolve to them. */
|
|
53
|
+
friendUserId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Synthetic inbound envelope for the friend greeting turn. Same invariant as
|
|
58
|
+
* `buildActivationBootstrapEnvelope`: `conversationId` MUST be a conversation
|
|
59
|
+
* idcode — the agent's reply inherits this chat_id, and anything else is
|
|
60
|
+
* refused at the outbound boundary after a full LLM turn has been spent.
|
|
61
|
+
*/
|
|
62
|
+
export function buildFriendGreetingEnvelope(params: BuildFriendGreetingEnvelopeParams): Envelope {
|
|
63
|
+
const { account, conversationId, friendUserId } = params;
|
|
64
|
+
const text = buildFriendGreetingText();
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
return {
|
|
67
|
+
version: "2",
|
|
68
|
+
event: EVENT.MESSAGE_SEND,
|
|
69
|
+
trace_id: `clawchat-plugin-openclaw-friend-greeting-${now}`,
|
|
70
|
+
emitted_at: now,
|
|
71
|
+
chat_id: conversationId,
|
|
72
|
+
chat_type: "direct",
|
|
73
|
+
to: { id: account.userId, type: "direct" },
|
|
74
|
+
sender: { id: friendUserId, type: "direct", nick_name: "" },
|
|
75
|
+
payload: {
|
|
76
|
+
message_id: `clawchat-plugin-openclaw-friend-greeting-${conversationId}-${now}`,
|
|
77
|
+
message_mode: "normal",
|
|
78
|
+
message: {
|
|
79
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
80
|
+
context: { mentions: [], reply: null },
|
|
81
|
+
streaming: {
|
|
82
|
+
status: "static",
|
|
83
|
+
sequence: 0,
|
|
84
|
+
mutation_policy: "sealed",
|
|
85
|
+
started_at: null,
|
|
86
|
+
completed_at: null,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
} as unknown as Envelope;
|
|
91
|
+
}
|