@clawling/clawchat-plugin-openclaw 2026.10.7-1 → 2026.10.7-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/framework-error-copy.js +172 -0
- package/dist/src/liveware-sample.js +106 -19
- package/dist/src/permission-result.js +116 -27
- package/dist/src/profile-prompt.js +17 -2
- package/dist/src/reply-dispatcher.js +12 -2
- package/dist/src/runtime.js +116 -2
- package/dist/src/skill-update.js +1 -1
- package/dist/src/tools-schema.js +1 -1
- package/dist/src/tools.js +90 -8
- package/package.json +3 -8
- package/skills/clawchat-core/SKILL.md +12 -3
- package/skills/clawchat-orchestration/SKILL.md +2 -1
- package/skills/clawchat-set-greeting/SKILL.md +5 -4
- package/skills/manifest.json +25 -25
- package/src/framework-error-copy.ts +185 -0
- package/src/liveware-sample.ts +106 -19
- package/src/permission-result.ts +134 -30
- package/src/profile-prompt.ts +21 -2
- package/src/reply-dispatcher.ts +24 -1
- package/src/runtime.ts +119 -3
- package/src/skill-update.ts +1 -1
- package/src/tools-schema.ts +1 -1
- package/src/tools.ts +99 -8
package/dist/src/runtime.js
CHANGED
|
@@ -859,6 +859,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
859
859
|
// `sendOwnerSkillMessage` below) — both live in this same function closure, so
|
|
860
860
|
// the forward reference here just resolves to the fresh value at call time.
|
|
861
861
|
let livewareSampleNotify = async () => false;
|
|
862
|
+
// The supervisor this gateway call shares (new or adopted). The abort cleanup
|
|
863
|
+
// below awaits its stop() so the host's channel stop (which waits on this
|
|
864
|
+
// task) does not return while liveware children are still being killed.
|
|
865
|
+
let livewareSampleForShutdown = null;
|
|
862
866
|
if (store) {
|
|
863
867
|
const stateDirForSample = runtime.state?.resolveStateDir?.();
|
|
864
868
|
const livewareSampleDeps = {
|
|
@@ -901,6 +905,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
901
905
|
livewareSampleSupervisors.set(abortSignal, supervisorsForSignal);
|
|
902
906
|
const supervisorKey = `openclaw:${accountId}`;
|
|
903
907
|
const runningSupervisor = supervisorsForSignal.get(supervisorKey);
|
|
908
|
+
livewareSampleForShutdown = runningSupervisor ?? null;
|
|
904
909
|
if (runningSupervisor) {
|
|
905
910
|
// A gateway re-entry (refresh/backoff reconnect), not a fresh channel start:
|
|
906
911
|
// hand the live supervisor this closure's token + REST client + notify path
|
|
@@ -910,9 +915,12 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
910
915
|
}
|
|
911
916
|
else {
|
|
912
917
|
const livewareSampleSupervisor = new LivewareSampleSupervisor(livewareSampleDeps);
|
|
918
|
+
livewareSampleForShutdown = livewareSampleSupervisor;
|
|
913
919
|
supervisorsForSignal.set(supervisorKey, livewareSampleSupervisor);
|
|
914
920
|
abortSignal.addEventListener("abort", () => {
|
|
915
|
-
|
|
921
|
+
// Starts the SIGTERM → SIGKILL teardown at once; the gateway's own
|
|
922
|
+
// abort cleanup awaits the same (idempotent) stop() promise.
|
|
923
|
+
void livewareSampleSupervisor.stop();
|
|
916
924
|
if (supervisorsForSignal.get(supervisorKey) === livewareSampleSupervisor) {
|
|
917
925
|
supervisorsForSignal.delete(supervisorKey);
|
|
918
926
|
}
|
|
@@ -1188,6 +1196,25 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1188
1196
|
const persistOwnerProfile = upsertOwnerProfile
|
|
1189
1197
|
? makeOwnerProfilePersister({ upsertOwnerProfile }, { accountId })
|
|
1190
1198
|
: undefined;
|
|
1199
|
+
// The owner_profile row is where owner-language copy reads its locale
|
|
1200
|
+
// (framework errors, greetings, liveware intros). It was only refreshed on
|
|
1201
|
+
// owner metadata pulls, so an owner who switched app language kept getting
|
|
1202
|
+
// the old one. friend.profile_updated also reaches the agent when the
|
|
1203
|
+
// owner edits their profile; refetch just the owner row then.
|
|
1204
|
+
const refreshOwnerProfileOnUpdate = (entityId) => {
|
|
1205
|
+
const subject = typeof entityId === "string" ? entityId.trim() : "";
|
|
1206
|
+
if (!persistOwnerProfile || !subject || subject !== account.ownerUserId)
|
|
1207
|
+
return;
|
|
1208
|
+
void (async () => {
|
|
1209
|
+
try {
|
|
1210
|
+
const owner = await getConversationApiClient().getAgentOwner();
|
|
1211
|
+
persistOwnerProfile(owner);
|
|
1212
|
+
}
|
|
1213
|
+
catch (err) {
|
|
1214
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw owner profile refresh failed source=friend.profile_updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1215
|
+
}
|
|
1216
|
+
})();
|
|
1217
|
+
};
|
|
1191
1218
|
const refreshAgentBehavior = async (options) => {
|
|
1192
1219
|
try {
|
|
1193
1220
|
await pullOwnerMetadata({
|
|
@@ -2305,6 +2332,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2305
2332
|
if (type === "friend.added") {
|
|
2306
2333
|
scheduleFriendGreeting(payload);
|
|
2307
2334
|
}
|
|
2335
|
+
if (type === "friend.profile_updated") {
|
|
2336
|
+
refreshOwnerProfileOnUpdate(payload?.entity_id);
|
|
2337
|
+
}
|
|
2308
2338
|
}
|
|
2309
2339
|
}
|
|
2310
2340
|
});
|
|
@@ -2409,6 +2439,8 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2409
2439
|
? groupFile.metadata.group_owner_nickname ?? metadata?.nickname ?? userId
|
|
2410
2440
|
: metadata?.nickname ?? userId;
|
|
2411
2441
|
const profileType = metadata?.profile_type ?? (userId === account.userId ? "agent" : "user");
|
|
2442
|
+
if (name !== userId)
|
|
2443
|
+
knownNicknames.set(userId, name);
|
|
2412
2444
|
participants.push({ id: userId, name, profileType, isAgentOwner, isGroupOwner });
|
|
2413
2445
|
}
|
|
2414
2446
|
return participants;
|
|
@@ -2444,6 +2476,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2444
2476
|
basePrompt: turn.peer.kind === "group" ? getClawChatGroupPrompt() : getClawChatUserPrompt(),
|
|
2445
2477
|
...promptMetadata,
|
|
2446
2478
|
ownerIdFallback: account.ownerUserId,
|
|
2479
|
+
currentAgentIdFallback: account.userId,
|
|
2447
2480
|
groupParticipants,
|
|
2448
2481
|
turn: {
|
|
2449
2482
|
chatType: promptChatType,
|
|
@@ -2463,9 +2496,76 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2463
2496
|
},
|
|
2464
2497
|
});
|
|
2465
2498
|
};
|
|
2499
|
+
// Group frames often carry the sender's id as nick_name. Prefer the nickname
|
|
2500
|
+
// already cached in ClawChat memory (owner.md for the owner and this agent,
|
|
2501
|
+
// users/<id>.md for everyone else, the group's group_owner_nickname for its
|
|
2502
|
+
// owner); keep the id when nothing is cached.
|
|
2503
|
+
const cachedNicknameFor = async (memoryRoot, userId) => {
|
|
2504
|
+
try {
|
|
2505
|
+
if (userId === account.ownerUserId || userId === account.userId) {
|
|
2506
|
+
const ownerFile = await readClawChatMemoryFile(memoryRoot, { targetType: "owner", targetId: "owner" });
|
|
2507
|
+
const nickname = userId === account.userId
|
|
2508
|
+
? ownerFile.metadata.agent_nickname
|
|
2509
|
+
: ownerFile.metadata.agent_owner_nickname;
|
|
2510
|
+
if (nickname)
|
|
2511
|
+
return nickname;
|
|
2512
|
+
}
|
|
2513
|
+
const userFile = await readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: userId });
|
|
2514
|
+
return userFile.exists && userFile.metadata.nickname ? userFile.metadata.nickname : null;
|
|
2515
|
+
}
|
|
2516
|
+
catch {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
// Nicknames seen while building group prompts, so ingest can label a
|
|
2521
|
+
// coalesced batch's transcript ("Alice:" rather than "usr_…:") without
|
|
2522
|
+
// touching disk.
|
|
2523
|
+
const knownNicknames = new Map();
|
|
2524
|
+
const withCachedGroupSenderNames = async (turn, memoryRoot) => {
|
|
2525
|
+
if (turn.peer.kind !== "group" || !memoryRoot)
|
|
2526
|
+
return turn;
|
|
2527
|
+
const isBareId = (name, id) => !name || name === id;
|
|
2528
|
+
const cache = new Map();
|
|
2529
|
+
let groupOwner = null;
|
|
2530
|
+
const lookup = async (userId) => {
|
|
2531
|
+
if (!cache.has(userId)) {
|
|
2532
|
+
let nickname = await cachedNicknameFor(memoryRoot, userId);
|
|
2533
|
+
if (!nickname) {
|
|
2534
|
+
if (!groupOwner) {
|
|
2535
|
+
try {
|
|
2536
|
+
const groupFile = await readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: turn.peer.id });
|
|
2537
|
+
groupOwner = { id: groupFile.metadata.group_owner_id, nickname: groupFile.metadata.group_owner_nickname };
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
groupOwner = {};
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
if (groupOwner.id === userId && groupOwner.nickname)
|
|
2544
|
+
nickname = groupOwner.nickname;
|
|
2545
|
+
}
|
|
2546
|
+
cache.set(userId, nickname);
|
|
2547
|
+
if (nickname)
|
|
2548
|
+
knownNicknames.set(userId, nickname);
|
|
2549
|
+
}
|
|
2550
|
+
return cache.get(userId) ?? null;
|
|
2551
|
+
};
|
|
2552
|
+
const senderNickName = isBareId(turn.senderNickName, turn.senderId)
|
|
2553
|
+
? (await lookup(turn.senderId)) ?? turn.senderNickName
|
|
2554
|
+
: turn.senderNickName;
|
|
2555
|
+
const groupMessages = turn.groupMessages
|
|
2556
|
+
? await Promise.all(turn.groupMessages.map(async (message) => isBareId(message.senderName, message.senderId)
|
|
2557
|
+
? { ...message, senderName: (await lookup(message.senderId)) ?? message.senderName }
|
|
2558
|
+
: message))
|
|
2559
|
+
: turn.groupMessages;
|
|
2560
|
+
return { ...turn, senderNickName, ...(groupMessages ? { groupMessages } : {}) };
|
|
2561
|
+
};
|
|
2466
2562
|
const resolveSenderNickNameForTurn = (turn) => {
|
|
2467
2563
|
if (turn.senderNickName && turn.senderNickName !== turn.senderId)
|
|
2468
2564
|
return turn.senderNickName;
|
|
2565
|
+
// Synchronous on purpose: an await here would reorder same-group frames.
|
|
2566
|
+
const known = turn.peer.kind === "group" ? knownNicknames.get(turn.senderId) : undefined;
|
|
2567
|
+
if (known)
|
|
2568
|
+
return known;
|
|
2469
2569
|
return turn.senderNickName || turn.senderId;
|
|
2470
2570
|
};
|
|
2471
2571
|
const resolveSenderBatchIdentityForTurn = (turn) => {
|
|
@@ -2480,7 +2580,9 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2480
2580
|
senderProfileType: turn.senderProfileType ??
|
|
2481
2581
|
(senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user"),
|
|
2482
2582
|
senderIsOwner: senderRelation === "owner",
|
|
2483
|
-
|
|
2583
|
+
// Left undefined when the frame does not say, so the prompt falls back
|
|
2584
|
+
// to the group metadata's group_owner_id instead of a forced false.
|
|
2585
|
+
...(turn.senderIsGroupOwner !== undefined ? { senderIsGroupOwner: turn.senderIsGroupOwner } : {}),
|
|
2484
2586
|
};
|
|
2485
2587
|
};
|
|
2486
2588
|
const claimInboundTurn = (turn) => {
|
|
@@ -2550,6 +2652,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2550
2652
|
catch (err) {
|
|
2551
2653
|
log?.error?.(`[${accountId}] clawchat-plugin-openclaw message metadata refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2552
2654
|
}
|
|
2655
|
+
turn = await withCachedGroupSenderNames(turn, memoryRoot);
|
|
2553
2656
|
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
2554
2657
|
const turnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
2555
2658
|
const senderIsOwner = turn.senderIsOwner === true || turn.senderId === account.ownerUserId;
|
|
@@ -2680,6 +2783,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2680
2783
|
runtime,
|
|
2681
2784
|
account,
|
|
2682
2785
|
client,
|
|
2786
|
+
resolveOwnerLocale: () => store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
2683
2787
|
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
2684
2788
|
...(replyCtx ? { replyCtx } : {}),
|
|
2685
2789
|
inboundMessageId: turn.messageId,
|
|
@@ -3270,6 +3374,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
3270
3374
|
closeReason: "client close",
|
|
3271
3375
|
});
|
|
3272
3376
|
client.close();
|
|
3377
|
+
if (livewareSampleForShutdown) {
|
|
3378
|
+
// SIGTERM, then SIGKILL after KILL_GRACE_MS (3 s): inside the host's
|
|
3379
|
+
// 5 s wait for this task, so a stuck child no longer outlives the stop.
|
|
3380
|
+
try {
|
|
3381
|
+
await livewareSampleForShutdown.stop();
|
|
3382
|
+
}
|
|
3383
|
+
catch (err) {
|
|
3384
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw liveware-sample stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3273
3387
|
setStatus({
|
|
3274
3388
|
...getStatus(),
|
|
3275
3389
|
connected: false,
|
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.17.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/dist/src/tools-schema.js
CHANGED
|
@@ -205,7 +205,7 @@ export const ClawchatCreateMomentSchema = Type.Object({
|
|
|
205
205
|
description: "Moment text. At least one of text or images is required.",
|
|
206
206
|
})),
|
|
207
207
|
images: Type.Optional(Type.Array(Type.String({
|
|
208
|
-
description: "
|
|
208
|
+
description: "An http(s) image URL, or an absolute local image path (the tool uploads it and uses the hosted URL).",
|
|
209
209
|
}))),
|
|
210
210
|
});
|
|
211
211
|
export const ClawchatDeleteMomentSchema = Type.Object({
|
package/dist/src/tools.js
CHANGED
|
@@ -28,6 +28,15 @@ function extractRequestId(err) {
|
|
|
28
28
|
}
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What the agent must do with the later `permission_result` receipt
|
|
33
|
+
* (agent-protocol §2.8): on approval the SERVER executes the gated call, so a
|
|
34
|
+
* second call would duplicate it or raise a fresh card. Only `approved_retry`
|
|
35
|
+
* (reads, which are never replayed) asks for exactly one more call.
|
|
36
|
+
*/
|
|
37
|
+
const PERMISSION_RECEIPT_RULE = "When the result arrives: outcome \"approved\" (or \"auto_allowed\") means the server has already done it for you — do not call this tool again; " +
|
|
38
|
+
"only outcome \"approved_retry\" means call this same tool once more with the same arguments. " +
|
|
39
|
+
"Anything the server produced (a new id, a code, read data) comes in that message's Result line.";
|
|
31
40
|
function permissionGateResult(err) {
|
|
32
41
|
const code = err.meta?.code;
|
|
33
42
|
const requestId = extractRequestId(err);
|
|
@@ -38,7 +47,8 @@ function permissionGateResult(err) {
|
|
|
38
47
|
"This operation requires the owner's approval and has been submitted for review" +
|
|
39
48
|
(requestId ? ` (request_id=${requestId})` : "") +
|
|
40
49
|
". It has NOT failed. Do not retry — the result will arrive later as a normal " +
|
|
41
|
-
"chat message; wait for it instead of calling this tool again."
|
|
50
|
+
"chat message; wait for it instead of calling this tool again. " +
|
|
51
|
+
PERMISSION_RECEIPT_RULE;
|
|
42
52
|
status = "pending";
|
|
43
53
|
}
|
|
44
54
|
else {
|
|
@@ -120,6 +130,46 @@ function inferMimeFromPath(filePath) {
|
|
|
120
130
|
const ext = path.extname(filePath).toLowerCase();
|
|
121
131
|
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
122
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Classify `clawchat_create_moment` image entries: http(s) URLs pass through;
|
|
135
|
+
* an existing absolute local regular file (≤ 20MB) is marked for upload; any
|
|
136
|
+
* other value is a validation error naming the offending index.
|
|
137
|
+
*/
|
|
138
|
+
function planMomentImages(images) {
|
|
139
|
+
const entries = [];
|
|
140
|
+
for (const [i, raw] of images.entries()) {
|
|
141
|
+
const where = `clawchat-plugin-openclaw: images[${i}]`;
|
|
142
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
143
|
+
if (!value) {
|
|
144
|
+
return { ok: false, message: `${where} must be an http(s) image URL or an absolute local image path` };
|
|
145
|
+
}
|
|
146
|
+
if (/^https?:\/\/\S+$/i.test(value)) {
|
|
147
|
+
entries.push({ kind: "url", url: value });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!path.isAbsolute(value)) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
message: `${where} (${JSON.stringify(value)}) is neither an http(s) URL nor an absolute local file path; no moment was created`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
let stat;
|
|
157
|
+
try {
|
|
158
|
+
stat = fs.statSync(value);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { ok: false, message: `${where}: local file ${value} does not exist; no moment was created` };
|
|
162
|
+
}
|
|
163
|
+
if (!stat.isFile()) {
|
|
164
|
+
return { ok: false, message: `${where}: ${value} is not a regular file; no moment was created` };
|
|
165
|
+
}
|
|
166
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
167
|
+
return { ok: false, message: `${where}: ${value} is too large (${stat.size} bytes; max 20MB)` };
|
|
168
|
+
}
|
|
169
|
+
entries.push({ kind: "local", path: value });
|
|
170
|
+
}
|
|
171
|
+
return { ok: true, entries };
|
|
172
|
+
}
|
|
123
173
|
function validateMentionMessageParams(params) {
|
|
124
174
|
if (!params || typeof params !== "object") {
|
|
125
175
|
return { ok: false, message: "clawchat-plugin-openclaw: params are required" };
|
|
@@ -285,6 +335,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
285
335
|
ok: true,
|
|
286
336
|
client: createOpenclawClawlingApiClient({
|
|
287
337
|
baseUrl: acct.baseUrl,
|
|
338
|
+
mediaBaseUrl: acct.mediaBaseUrl,
|
|
288
339
|
token: acct.token,
|
|
289
340
|
userId: acct.userId,
|
|
290
341
|
}),
|
|
@@ -329,8 +380,16 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
329
380
|
? rawData
|
|
330
381
|
: {};
|
|
331
382
|
const outcome = mapGateOutcome(code, gateData);
|
|
332
|
-
if (outcome !== null)
|
|
333
|
-
|
|
383
|
+
if (outcome !== null) {
|
|
384
|
+
// `next_step` is added here, not in mapGateOutcome: that mapping
|
|
385
|
+
// is pinned byte-for-byte against the Hermes parity fixtures.
|
|
386
|
+
return jsonResponse(outcome.status === "pending_owner_approval"
|
|
387
|
+
? {
|
|
388
|
+
...outcome,
|
|
389
|
+
next_step: "Waiting for the owner's approval; do not call this tool again now. " + PERMISSION_RECEIPT_RULE,
|
|
390
|
+
}
|
|
391
|
+
: outcome);
|
|
392
|
+
}
|
|
334
393
|
}
|
|
335
394
|
return apiError(err);
|
|
336
395
|
}
|
|
@@ -1100,7 +1159,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1100
1159
|
label: "Create ClawChat Moment",
|
|
1101
1160
|
description: toolDescription("Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
|
|
1102
1161
|
"TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"publish a dynamic ...\", or \"send a friend moment ...\". " +
|
|
1103
|
-
"At least one of text or images must be present.
|
|
1162
|
+
"At least one of text or images must be present. Each images entry is either an http(s) image URL (used as-is) or an absolute local image path (max 20MB) — for a local path this tool uploads it to ClawChat media itself and uses the hosted URL, so no separate upload step is needed. Any other value (relative path, file:// or data: URI, missing file) is rejected and no moment is created."),
|
|
1104
1163
|
parameters: ClawchatCreateMomentSchema,
|
|
1105
1164
|
async execute(_callId, params) {
|
|
1106
1165
|
return await recordClawchatToolCall(accountId, "clawchat_create_moment", params, async () => {
|
|
@@ -1110,10 +1169,33 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1110
1169
|
if (!text && (!images || images.length === 0)) {
|
|
1111
1170
|
return validationError("clawchat-plugin-openclaw: at least one of text or images is required");
|
|
1112
1171
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1172
|
+
// Validate every entry before touching the network, so a bad value
|
|
1173
|
+
// never leaves a half-uploaded batch or a moment with a broken image.
|
|
1174
|
+
const plan = images !== undefined ? planMomentImages(images) : { ok: true, entries: [] };
|
|
1175
|
+
if (!plan.ok)
|
|
1176
|
+
return validationError(plan.message);
|
|
1177
|
+
return await withGatedClient(accountId, async (c) => {
|
|
1178
|
+
const resolved = [];
|
|
1179
|
+
for (const entry of plan.entries) {
|
|
1180
|
+
if (entry.kind === "url") {
|
|
1181
|
+
resolved.push(entry.url);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const uploaded = await c.uploadMedia({
|
|
1185
|
+
buffer: fs.readFileSync(entry.path),
|
|
1186
|
+
filename: path.basename(entry.path),
|
|
1187
|
+
mime: inferMimeFromPath(entry.path),
|
|
1188
|
+
});
|
|
1189
|
+
if (uploaded.kind !== "image") {
|
|
1190
|
+
throw new Error(`clawchat-plugin-openclaw: ${entry.path} is not an image (media service classified it as ${uploaded.kind}); no moment was created`);
|
|
1191
|
+
}
|
|
1192
|
+
resolved.push(uploaded.url);
|
|
1193
|
+
}
|
|
1194
|
+
return await c.createMoment({
|
|
1195
|
+
...(text !== undefined ? { text } : {}),
|
|
1196
|
+
...(images !== undefined ? { images: resolved } : {}),
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1117
1199
|
});
|
|
1118
1200
|
},
|
|
1119
1201
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawling/clawchat-plugin-openclaw",
|
|
3
|
-
"version": "2026.10.7-
|
|
3
|
+
"version": "2026.10.7-2",
|
|
4
4
|
"description": "OpenClaw ClawChat channel plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CLAWLING PTE. LTD.",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"build": "tsc -p tsconfig.build.json",
|
|
43
43
|
"test": "vitest",
|
|
44
44
|
"dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
45
|
-
"prepack": "npm run build",
|
|
45
|
+
"prepack": "npm run build && node tools/pack-manifest.mjs strip",
|
|
46
|
+
"postpack": "node tools/pack-manifest.mjs restore",
|
|
46
47
|
"typecheck": "tsc --noEmit",
|
|
47
48
|
"prepublishOnly": "npm run typecheck",
|
|
48
49
|
"release": "npm run prepublishOnly && npm publish --tag latest"
|
|
@@ -50,12 +51,6 @@
|
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@sinclair/typebox": "0.34.48"
|
|
52
53
|
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "^25.5.0",
|
|
55
|
-
"openclaw": "2026.6.11",
|
|
56
|
-
"typescript": "^5.4.0",
|
|
57
|
-
"vitest": "^4.1.5"
|
|
58
|
-
},
|
|
59
54
|
"peerDependencies": {
|
|
60
55
|
"openclaw": ">=2026.5.4"
|
|
61
56
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-core
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.10.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, managing the owner's other agents or groups, or plugin install/update/activation.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ This skill guides agent behavior for ClawChat-aware tasks. Use the registered Cl
|
|
|
18
18
|
|
|
19
19
|
## Sending an Image, File, or Voice Message
|
|
20
20
|
|
|
21
|
-
To deliver an image, file, or voice/audio clip into the current ClawChat conversation, use the OpenClaw **message tool** with `action='send'` and `media` set to a local file path or an HTTPS URL. This is a host message-tool capability, not a `clawchat_*` tool — the `
|
|
21
|
+
To deliver an image, file, or voice/audio clip into the current ClawChat conversation, use the OpenClaw **message tool** with `action='send'` and `media` set to a local file path or an HTTPS URL. This is a host message-tool capability, not a `clawchat_*` tool — `clawchat_upload_avatar_image` (avatars) and the `images` of `clawchat_create_moment` (moments) are separate surfaces and do not post into a conversation.
|
|
22
22
|
|
|
23
23
|
- ClawChat detects the media type from the file and renders it: images inline, audio files (`.mp3`, `.m4a`, `.wav`, `.ogg`, `.aac`, …) as **playable voice messages**, everything else as a downloadable file. There is no separate voice tool or `voice` kind — a voice message is just audio media, so sending a genuine audio file is how you "send a voice message".
|
|
24
24
|
- Use a real audio file with its normal extension so its type is recognized as audio; an extension-less or mislabeled file may arrive as a plain file. The clip length is shown on the recipient side automatically — you do not set a duration.
|
|
@@ -122,7 +122,7 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
|
|
|
122
122
|
| Add a person to a group | `clawchat_add_group_member` with the exact group `conversationId` and the person's exact `userId` (from group metadata, `clawchat_list_account_friends`, or `clawchat_search_users`; never guessed from a name), only on an explicit request. Groups only; the person must already be your friend. The owner's `group.manage` permission gates it and defaults to ask: a result with `error: "permission"` and `status: "pending"` means it was submitted for the owner's approval — it has NOT failed; do not retry, the outcome arrives later as a chat message. `status: "forbidden"` means the owner's policy blocks it; do not retry |
|
|
123
123
|
| View/browse moments or dynamics | `clawchat_list_moments` |
|
|
124
124
|
| Read one moment and its visible comments by exact id | `clawchat_get_moment` with exact `momentId`; read-only, use after a `moment.comment.created`/`moment.comment.replied` awareness note to read the new comment before deciding whether to reply |
|
|
125
|
-
| Create a moment/dynamic | `clawchat_create_moment`;
|
|
125
|
+
| Create a moment/dynamic | `clawchat_create_moment`; each `images` entry is an http(s) URL or an absolute local file path (the plugin uploads a local file for you); anything else is rejected |
|
|
126
126
|
| Delete a moment/dynamic | `clawchat_delete_moment` with an exact `momentId` |
|
|
127
127
|
| React/unreact to a moment | `clawchat_toggle_moment_reaction` with exact `momentId` and emoji |
|
|
128
128
|
| Top-level moment comment | `clawchat_create_moment_comment` |
|
|
@@ -130,6 +130,15 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
|
|
|
130
130
|
| Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
|
|
131
131
|
| Nickname or bio update | `clawchat_update_account_profile` |
|
|
132
132
|
|
|
133
|
+
## Owner Approvals (Permission Receipts)
|
|
134
|
+
|
|
135
|
+
Some operations are gated by the owner's permissions. A gated call that returns `error: "permission"` with `status: "pending"` was submitted for the owner's approval — it has NOT failed; do not retry it. The outcome arrives later as a permission receipt, a system message in your chat with the owner:
|
|
136
|
+
|
|
137
|
+
- **`approved`** — the server already carried out the operation for you. It is done: do not call it again. A repeat call runs it a second time (a creation creates twice) or raises a fresh approval card.
|
|
138
|
+
- **`approved_retry`** — the only outcome that asks you to call again: make the same call once more.
|
|
139
|
+
- **Approved reads** return their data in the receipt's `result` (for example a group invite `code`). Use what is there instead of calling the read again.
|
|
140
|
+
- `denied`, `expired`, or `failed` — tell the user what happened; do not retry on your own.
|
|
141
|
+
|
|
133
142
|
## Managing The Owner's Other Agents
|
|
134
143
|
|
|
135
144
|
When the owner asks you to manage their **other** agents or their **groups** —
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-orchestration
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.2.0
|
|
4
4
|
description: Use when the owner asks this agent to manage their OTHER ClawChat agents or their groups — 编排 / orchestrate a fleet, read or rewrite another agent's 提示词 / system prompt / behavior, 禁言 / mute an agent, change 回复模式 / reply mode, stop 刷屏 / flooding in a group, 建群 / create a group of agents, add or remove agents from a group, or 签发连接码 / issue a connect code.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -169,6 +169,7 @@ throw is **not** by itself success — read the `code` field.
|
|
|
169
169
|
| Code | Means | Do |
|
|
170
170
|
| --- | --- | --- |
|
|
171
171
|
| `21003` | Owner has not turned on 云端编排 / Cloud orchestration — **the common case** | Ask the owner to turn it on in your permission settings. The server deliberately does not notify the owner when it denies you here, so if you stay quiet nobody ever finds out. Do not retry |
|
|
172
|
+
| `21001` | The operation is waiting for the owner's approval; nothing has happened yet | Tell the owner it is waiting on them, then stop — do not call again now. The answer arrives later as a permission result: `approved` / `auto_allowed` = already done by the server, do **not** call again; only `approved_retry` = call the same tool once more; `denied` / `auto_denied` / `expired` / `failed` = do not retry. A `result` on it holds what the operation produced (e.g. `conversation_id`, connect `code`) — use that instead of calling again |
|
|
172
173
|
| `403` | Insufficient scope — the `agent:orchestrate` scope is missing (rare; it is a default scope) | Report. Do not retry |
|
|
173
174
|
| `401` | Credentials stale or revoked | Report once. Do not loop |
|
|
174
175
|
| `16025` | Connect-code rate limit; the bucket is your owner's, shared with their own manual issuance | Wait. Do not hammer it |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-set-greeting
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.1
|
|
4
4
|
description: Use when the user wants to customize, change, set, or reset this agent's greetings — the first-load / activation greeting to the owner (~/clawchat/greeting.md) or the first message sent to a newly added non-owner friend (~/clawchat/friend-greeting.md).
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -58,8 +58,8 @@ Override it the same way with **`~/clawchat/friend-greeting.md`**: same rules as
|
|
|
58
58
|
is an instruction to you, not the literal message; keep it short; no secrets; it is a
|
|
59
59
|
partial override that the plugin may still append its own trailing line to). Delete or
|
|
60
60
|
empty the file to restore the built-in instruction. The owner can turn this greeting off
|
|
61
|
-
entirely in the plugin config (`
|
|
62
|
-
|
|
61
|
+
entirely in the plugin config (`friendGreeting: false`); it is not something you can
|
|
62
|
+
disable from chat.
|
|
63
63
|
|
|
64
64
|
When the user asks about "the greeting" without saying which, ask whether they mean the
|
|
65
65
|
owner activation greeting or the new-friend greeting.
|
|
@@ -69,4 +69,5 @@ owner activation greeting or the new-friend greeting.
|
|
|
69
69
|
- `greeting.md` affects only the **first-load** activation greeting to the owner;
|
|
70
70
|
`friend-greeting.md` affects only the first message to a newly added non-owner friend.
|
|
71
71
|
Neither changes later replies.
|
|
72
|
-
-
|
|
72
|
+
- These are the OpenClaw files. A Hermes agent on the same machine does not read them: it
|
|
73
|
+
keeps its own copies inside each Hermes profile.
|
package/skills/manifest.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"skills": {
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"clawchat-core": {
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.10.0",
|
|
7
7
|
"path": "openclaw/clawchat-core/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
8
|
+
"sha256": "acaf36b1e7c477a707ed615b07cc1b87770e5c205e1e89e1c2a00519e964ac25",
|
|
9
|
+
"bytes": 17683
|
|
10
10
|
},
|
|
11
11
|
"clawchat-liveware": {
|
|
12
12
|
"version": "1.2.3",
|
|
13
|
-
"path": "
|
|
13
|
+
"path": "openclaw/clawchat-liveware/SKILL.md",
|
|
14
14
|
"sha256": "2ed150bad7972d452b5b437d7c524ab24c27b0677419e590f087f50b7e7a3cdd",
|
|
15
15
|
"bytes": 12591
|
|
16
16
|
},
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"bytes": 8892
|
|
22
22
|
},
|
|
23
23
|
"clawchat-set-greeting": {
|
|
24
|
-
"version": "1.2.
|
|
25
|
-
"path": "
|
|
26
|
-
"sha256": "
|
|
27
|
-
"bytes":
|
|
24
|
+
"version": "1.2.1",
|
|
25
|
+
"path": "openclaw/clawchat-set-greeting/SKILL.md",
|
|
26
|
+
"sha256": "11d9195fa11c1d23cfe282f28f69ce899ae37bdcbfc79fecebee64f4f1a803da",
|
|
27
|
+
"bytes": 4195
|
|
28
28
|
},
|
|
29
29
|
"clawchat-liveware-sample": {
|
|
30
30
|
"version": "2.0.1",
|
|
@@ -33,24 +33,24 @@
|
|
|
33
33
|
"bytes": 13943
|
|
34
34
|
},
|
|
35
35
|
"clawchat-orchestration": {
|
|
36
|
-
"version": "2.
|
|
36
|
+
"version": "2.2.0",
|
|
37
37
|
"path": "shared/clawchat-orchestration/SKILL.md",
|
|
38
|
-
"sha256": "
|
|
39
|
-
"bytes":
|
|
38
|
+
"sha256": "5f7319be1c77dfa265c8bce343d9d28ad52c2285667926b7fe8ba273d593d03c",
|
|
39
|
+
"bytes": 11700
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"hermes": {
|
|
43
43
|
"clawchat-core": {
|
|
44
|
-
"version": "1.
|
|
44
|
+
"version": "1.17.0",
|
|
45
45
|
"path": "hermes/clawchat-core/SKILL.md",
|
|
46
|
-
"sha256": "
|
|
47
|
-
"bytes":
|
|
46
|
+
"sha256": "b3038b3a540645a19224ce198fdf77f1fbb109b843a3a80fe803a955d027d689",
|
|
47
|
+
"bytes": 27668
|
|
48
48
|
},
|
|
49
49
|
"clawchat-liveware": {
|
|
50
|
-
"version": "1.
|
|
51
|
-
"path": "
|
|
52
|
-
"sha256": "
|
|
53
|
-
"bytes":
|
|
50
|
+
"version": "1.3.0",
|
|
51
|
+
"path": "hermes/clawchat-liveware/SKILL.md",
|
|
52
|
+
"sha256": "6bc6fff06c0694bf02323eff42926f15e7b6d6ed744e66b23a971f9290a9bb28",
|
|
53
|
+
"bytes": 14562
|
|
54
54
|
},
|
|
55
55
|
"clawchat-liveware-dev": {
|
|
56
56
|
"version": "1.0.0",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"bytes": 8892
|
|
60
60
|
},
|
|
61
61
|
"clawchat-set-greeting": {
|
|
62
|
-
"version": "1.
|
|
63
|
-
"path": "
|
|
64
|
-
"sha256": "
|
|
65
|
-
"bytes":
|
|
62
|
+
"version": "1.3.0",
|
|
63
|
+
"path": "hermes/clawchat-set-greeting/SKILL.md",
|
|
64
|
+
"sha256": "5092a2df48dfd61495bcd1f3be8f6603ca214b7f33afaddcf2d7c28442e91328",
|
|
65
|
+
"bytes": 5797
|
|
66
66
|
},
|
|
67
67
|
"clawchat-liveware-sample": {
|
|
68
68
|
"version": "2.0.1",
|
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
"bytes": 13843
|
|
72
72
|
},
|
|
73
73
|
"clawchat-orchestration": {
|
|
74
|
-
"version": "2.
|
|
74
|
+
"version": "2.2.0",
|
|
75
75
|
"path": "shared/clawchat-orchestration/SKILL.md",
|
|
76
|
-
"sha256": "
|
|
77
|
-
"bytes":
|
|
76
|
+
"sha256": "5f7319be1c77dfa265c8bce343d9d28ad52c2285667926b7fe8ba273d593d03c",
|
|
77
|
+
"bytes": 11700
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
},
|