@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/runtime.ts
CHANGED
|
@@ -17,7 +17,9 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
|
17
17
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
18
18
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
19
19
|
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
20
|
+
import { buildFriendGreetingEnvelope } from "./friend-greeting.ts";
|
|
20
21
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
22
|
+
import { readOnboardingReport } from "./onboarding-report.ts";
|
|
21
23
|
import path from "node:path";
|
|
22
24
|
import {
|
|
23
25
|
ensureLivewareCli,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
} from "./liveware-sample.ts";
|
|
33
35
|
import { ClawlingApiError } from "./api-types.ts";
|
|
34
36
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
37
|
+
import { RECONNECT_GUIDE_URL } from "./onboarding-context.ts";
|
|
35
38
|
import {
|
|
36
39
|
runOpenclawClawlingLogin,
|
|
37
40
|
type LoginParams,
|
|
@@ -209,9 +212,13 @@ const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
|
|
|
209
212
|
]);
|
|
210
213
|
const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
|
|
211
214
|
// §C.1 — user-visible message emitted on permanent token expiry. Kept
|
|
212
|
-
// byte-identical to the Hermes plugin (parity spec §C.1.4).
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
".";
|
|
215
222
|
const CLAWCHAT_TOKEN_EXPIRED_LAST_ERROR = "token expired — re-pair required";
|
|
216
223
|
|
|
217
224
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -1066,6 +1073,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1066
1073
|
pluginVersion,
|
|
1067
1074
|
agentVersion,
|
|
1068
1075
|
authenticated: false,
|
|
1076
|
+
onboarding: readOnboardingReport(),
|
|
1069
1077
|
log,
|
|
1070
1078
|
});
|
|
1071
1079
|
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
@@ -1138,6 +1146,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1138
1146
|
pluginVersion,
|
|
1139
1147
|
agentVersion,
|
|
1140
1148
|
authenticated: true,
|
|
1149
|
+
onboarding: readOnboardingReport(),
|
|
1141
1150
|
log,
|
|
1142
1151
|
});
|
|
1143
1152
|
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
|
@@ -2368,6 +2377,39 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2368
2377
|
],
|
|
2369
2378
|
}),
|
|
2370
2379
|
);
|
|
2380
|
+
} else if (env.event === "history.truncated") {
|
|
2381
|
+
// Replay-start boundary frame (docs/client-integration.md §11.7): part
|
|
2382
|
+
// of the retained history will not be replayed to this device. This
|
|
2383
|
+
// plugin advertises no reliable-delivery flag, so the server does not
|
|
2384
|
+
// send it today; if one arrives it is logged explicitly. Never acked (no
|
|
2385
|
+
// seq/dseq), never dispatched, no user-visible action. An absent or
|
|
2386
|
+
// unrecognised `reason` means "pruned"; `oldest_seq` is an opaque token.
|
|
2387
|
+
const current = wsLogContext();
|
|
2388
|
+
const truncatedPayload = env.payload && typeof env.payload === "object"
|
|
2389
|
+
? (env.payload as { oldest_seq?: unknown; reason?: unknown })
|
|
2390
|
+
: undefined;
|
|
2391
|
+
const rawReason = truncatedPayload?.reason;
|
|
2392
|
+
const knownReason = rawReason === "pruned" || rawReason === "cursor_started_above_zero";
|
|
2393
|
+
const fields: Array<[string, string | number]> = [
|
|
2394
|
+
["event_name", env.event],
|
|
2395
|
+
["trace_id", env.trace_id],
|
|
2396
|
+
["oldest_seq", typeof truncatedPayload?.oldest_seq === "number" ? truncatedPayload.oldest_seq : "-"],
|
|
2397
|
+
["reason", knownReason ? (rawReason as string) : "pruned"],
|
|
2398
|
+
];
|
|
2399
|
+
if (!knownReason && typeof rawReason === "string" && rawReason) {
|
|
2400
|
+
fields.push(["raw_reason", rawReason]);
|
|
2401
|
+
}
|
|
2402
|
+
log?.info?.(
|
|
2403
|
+
formatWsLog({
|
|
2404
|
+
event: "inbound_control",
|
|
2405
|
+
accountId,
|
|
2406
|
+
attempt: current.attempt,
|
|
2407
|
+
reconnectCount: current.reconnectCount,
|
|
2408
|
+
state: "ready",
|
|
2409
|
+
action: "history_truncated",
|
|
2410
|
+
fields,
|
|
2411
|
+
}),
|
|
2412
|
+
);
|
|
2371
2413
|
} else if (env.event !== "ping" && env.event !== "pong") {
|
|
2372
2414
|
const current = wsLogContext();
|
|
2373
2415
|
log?.info?.(
|
|
@@ -2527,6 +2569,93 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2527
2569
|
// A single queueMicrotask coalesces all awareness signals that arrive in
|
|
2528
2570
|
// the same synchronous turn into exactly one handleInboundEnvelope call.
|
|
2529
2571
|
let pendingAwarenessNote = false;
|
|
2572
|
+
/**
|
|
2573
|
+
* First message to a newly added NON-owner friend (`friend.added`).
|
|
2574
|
+
*
|
|
2575
|
+
* The signal only names the counterparty; the direct conversation the
|
|
2576
|
+
* server created inside the accept transaction is resolved through
|
|
2577
|
+
* `POST /v1/conversations/direct` and ONE synthetic greeting turn runs in
|
|
2578
|
+
* it. Dedupe is persisted through the message ledger keyed on the signal's
|
|
2579
|
+
* `event_id` (`message_id` is `notify:friend.added:<userId>` and would
|
|
2580
|
+
* collide on remove + re-add, so it is only the fallback key) — a
|
|
2581
|
+
* reconnect replay of the same signal cannot greet twice. The claim uses
|
|
2582
|
+
* kind "message" because the ledger's once-only unique index is partial on
|
|
2583
|
+
* kind = 'message'; chat_id stays null so the row never feeds a transcript.
|
|
2584
|
+
* The owner's own `friend.added` is skipped: the activation bootstrap greets
|
|
2585
|
+
* the owner.
|
|
2586
|
+
*/
|
|
2587
|
+
const scheduleFriendGreeting = (payload: Record<string, unknown> | undefined): void => {
|
|
2588
|
+
if (!account.friendGreeting) return;
|
|
2589
|
+
const friendUserId = typeof payload?.entity_id === "string" ? payload.entity_id.trim() : "";
|
|
2590
|
+
if (!friendUserId) return;
|
|
2591
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
2592
|
+
if (ownerUserId && friendUserId === ownerUserId) return;
|
|
2593
|
+
const eventId = typeof payload?.event_id === "string" ? payload.event_id.trim() : "";
|
|
2594
|
+
const messageId = typeof payload?.message_id === "string" ? payload.message_id.trim() : "";
|
|
2595
|
+
const dedupeKey = eventId || messageId;
|
|
2596
|
+
if (!dedupeKey) {
|
|
2597
|
+
log?.info?.(
|
|
2598
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_dedupe_key`,
|
|
2599
|
+
);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (store?.claimMessageOnce) {
|
|
2603
|
+
const claimed = recordConnection("friend greeting claim", () =>
|
|
2604
|
+
store.claimMessageOnce?.({
|
|
2605
|
+
platform: "openclaw",
|
|
2606
|
+
accountId,
|
|
2607
|
+
kind: "message",
|
|
2608
|
+
direction: "inbound",
|
|
2609
|
+
eventType: "friend.greeting",
|
|
2610
|
+
traceId: null,
|
|
2611
|
+
chatId: null,
|
|
2612
|
+
messageId: `friend.greeting:${dedupeKey}`,
|
|
2613
|
+
text: null,
|
|
2614
|
+
raw: { friend_user_id: friendUserId },
|
|
2615
|
+
}),
|
|
2616
|
+
);
|
|
2617
|
+
if (claimed !== true) {
|
|
2618
|
+
// false = already greeted (replay); null = ledger undecided — fail
|
|
2619
|
+
// closed rather than risk a duplicate unsolicited message.
|
|
2620
|
+
log?.info?.(
|
|
2621
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} key=${dedupeKey} claimed=${String(claimed)}`,
|
|
2622
|
+
);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
void (async () => {
|
|
2627
|
+
let conversationId = "";
|
|
2628
|
+
try {
|
|
2629
|
+
const result = await getConversationApiClient().getDirectConversation(friendUserId);
|
|
2630
|
+
conversationId = result?.conversation?.id?.trim() ?? "";
|
|
2631
|
+
} catch (err) {
|
|
2632
|
+
log?.error?.(
|
|
2633
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting conversation lookup failed friend=${friendUserId}: ${
|
|
2634
|
+
err instanceof Error ? err.message : String(err)
|
|
2635
|
+
}`,
|
|
2636
|
+
);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (!isValidChatId(conversationId)) {
|
|
2640
|
+
log?.error?.(
|
|
2641
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_conversation_id`,
|
|
2642
|
+
);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
log?.info?.(
|
|
2646
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting dispatch friend=${friendUserId} chat_id=${conversationId}`,
|
|
2647
|
+
);
|
|
2648
|
+
await handleInboundEnvelope(
|
|
2649
|
+
buildFriendGreetingEnvelope({ account, conversationId, friendUserId }),
|
|
2650
|
+
);
|
|
2651
|
+
})().catch((err) => {
|
|
2652
|
+
log?.error?.(
|
|
2653
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting failed friend=${friendUserId}: ${
|
|
2654
|
+
err instanceof Error ? err.message : String(err)
|
|
2655
|
+
}`,
|
|
2656
|
+
);
|
|
2657
|
+
});
|
|
2658
|
+
};
|
|
2530
2659
|
|
|
2531
2660
|
client.on("notify:signal", (env: Envelope) => {
|
|
2532
2661
|
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
@@ -2641,6 +2770,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2641
2770
|
);
|
|
2642
2771
|
});
|
|
2643
2772
|
}
|
|
2773
|
+
if (type === "friend.added") {
|
|
2774
|
+
scheduleFriendGreeting(payload as Record<string, unknown> | undefined);
|
|
2775
|
+
}
|
|
2644
2776
|
}
|
|
2645
2777
|
}
|
|
2646
2778
|
});
|
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.10.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;
|
package/src/tools-schema.ts
CHANGED
|
@@ -201,6 +201,14 @@ export const ClawchatGetConversationSchema = Type.Object({
|
|
|
201
201
|
});
|
|
202
202
|
export type ClawchatGetConversationParams = Static<typeof ClawchatGetConversationSchema>;
|
|
203
203
|
|
|
204
|
+
export const ClawchatGetDirectConversationSchema = Type.Object({
|
|
205
|
+
userId: Type.String({
|
|
206
|
+
minLength: 1,
|
|
207
|
+
description: "Concrete ClawChat user id (usr_...) of the friend",
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
export type ClawchatGetDirectConversationParams = Static<typeof ClawchatGetDirectConversationSchema>;
|
|
211
|
+
|
|
204
212
|
export const ClawchatLeaveGroupSchema = Type.Object({
|
|
205
213
|
conversationId: Type.String({
|
|
206
214
|
description: "Concrete ClawChat group conversation id to leave",
|
package/src/tools.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import {
|
|
45
45
|
ClawchatGetAccountProfileSchema,
|
|
46
46
|
ClawchatGetConversationSchema,
|
|
47
|
+
ClawchatGetDirectConversationSchema,
|
|
47
48
|
ClawchatGetMomentSchema,
|
|
48
49
|
ClawchatGetUserProfileSchema,
|
|
49
50
|
ClawchatLeaveGroupSchema,
|
|
@@ -88,6 +89,7 @@ import {
|
|
|
88
89
|
type ClawchatDeleteMomentParams,
|
|
89
90
|
type ClawchatAcceptFriendRequestParams,
|
|
90
91
|
type ClawchatGetConversationParams,
|
|
92
|
+
type ClawchatGetDirectConversationParams,
|
|
91
93
|
type ClawchatGetMomentParams,
|
|
92
94
|
type ClawchatGetUserProfileParams,
|
|
93
95
|
type ClawchatLeaveGroupParams,
|
|
@@ -1090,6 +1092,41 @@ export function registerOpenclawClawlingTools(
|
|
|
1090
1092
|
{ name: "clawchat_get_conversation" },
|
|
1091
1093
|
);
|
|
1092
1094
|
|
|
1095
|
+
api.registerTool(
|
|
1096
|
+
(ctx) => {
|
|
1097
|
+
const accountId = resolveOpenclawClawlingToolAccountId(ctx);
|
|
1098
|
+
return {
|
|
1099
|
+
name: "clawchat_get_direct_conversation",
|
|
1100
|
+
label: "Get ClawChat Direct Conversation With User",
|
|
1101
|
+
description: toolDescription(
|
|
1102
|
+
"Resolve the direct (1:1) ClawChat conversation with a specific user to its conversation id (cnv_...), creating it if needed. " +
|
|
1103
|
+
"TRIGGER - invoke when you need to send a message to a ClawChat user and only know their userId (usr_...), for example to start a conversation with a newly added friend. " +
|
|
1104
|
+
"The user must already be your friend; otherwise the server rejects the call. " +
|
|
1105
|
+
"Use the returned conversation.id as chatId for clawchat_mention_message. " +
|
|
1106
|
+
"Never pass a userId or a name as a chatId.",
|
|
1107
|
+
),
|
|
1108
|
+
parameters: ClawchatGetDirectConversationSchema,
|
|
1109
|
+
async execute(_callId, params) {
|
|
1110
|
+
return await recordClawchatToolCall(accountId, "clawchat_get_direct_conversation", params, async () => {
|
|
1111
|
+
const p = params as ClawchatGetDirectConversationParams;
|
|
1112
|
+
const built = buildClient(accountId);
|
|
1113
|
+
if (!built.ok) return built.error;
|
|
1114
|
+
try {
|
|
1115
|
+
const data = await built.client.getDirectConversation(p.userId);
|
|
1116
|
+
return jsonResponse(data);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
if (err instanceof ClawlingApiError) {
|
|
1119
|
+
return apiError(err);
|
|
1120
|
+
}
|
|
1121
|
+
return genericError(err);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
{ name: "clawchat_get_direct_conversation" },
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1093
1130
|
api.registerTool(
|
|
1094
1131
|
(ctx) => {
|
|
1095
1132
|
const accountId = resolveOpenclawClawlingToolAccountId(ctx);
|
package/src/ws-client.ts
CHANGED
|
@@ -733,6 +733,10 @@ export class ClawChatClient extends EventEmitter {
|
|
|
733
733
|
if (env.event === EVENT.NOTIFY_SIGNAL) this.noteConversationSignalAlive(env);
|
|
734
734
|
if (env.event === EVENT.NOTIFY_SIGNAL) this.emit("notify:signal", env);
|
|
735
735
|
if (env.event === EVENT.REPLAY_DONE) this.emit("replay:done", env);
|
|
736
|
+
// Replay-start boundary frame (docs/client-integration.md §11.7). Control
|
|
737
|
+
// only: it carries no seq/dseq, so it is never acked and never becomes a
|
|
738
|
+
// `message`. Its own emitter keeps it out of the unknown-event path.
|
|
739
|
+
if (env.event === EVENT.HISTORY_TRUNCATED) this.emit("history:truncated", env);
|
|
736
740
|
if (env.event === EVENT.OFFLINE_DONE) this.emit("offline:done");
|
|
737
741
|
}
|
|
738
742
|
|