@clawling/clawchat-plugin-openclaw 2026.8.2-1 → 2026.8.4-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/api-client.js +10 -0
- package/dist/src/liveware-sample.js +80 -10
- package/dist/src/skill-update.js +5 -3
- package/dist/src/tools-schema.js +10 -0
- package/dist/src/tools.js +29 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/src/api-client.ts +21 -0
- package/src/liveware-sample.ts +81 -4
- package/src/skill-update.ts +5 -3
- package/src/tools-schema.ts +12 -0
- package/src/tools.ts +34 -0
package/dist/src/api-client.js
CHANGED
|
@@ -398,6 +398,16 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
398
398
|
assertNonBlankId(conversationId, "leaveConversation: conversationId");
|
|
399
399
|
return await call("POST", `/v1/conversations/${encodeURIComponent(conversationId)}/leave`);
|
|
400
400
|
},
|
|
401
|
+
async addConversationMember(conversationId, userId) {
|
|
402
|
+
assertNonBlankId(conversationId, "addConversationMember: conversationId");
|
|
403
|
+
assertNonBlankId(userId, "addConversationMember: userId");
|
|
404
|
+
const cid = conversationId.trim();
|
|
405
|
+
const uid = userId.trim();
|
|
406
|
+
return await call("POST", `/v1/conversations/${encodeURIComponent(cid)}/members`, {
|
|
407
|
+
body: JSON.stringify({ user_id: uid }),
|
|
408
|
+
headers: { "content-type": "application/json" },
|
|
409
|
+
});
|
|
410
|
+
},
|
|
401
411
|
async patchConversation(conversationId, patch) {
|
|
402
412
|
assertNonBlankId(conversationId, "patchConversation: conversationId");
|
|
403
413
|
const body = pickPatch(patch, ["title", "description"], "patchConversation");
|
|
@@ -15,6 +15,35 @@ export const LIVEWARE_SAMPLE_APP_NAME = "Liveware Sample";
|
|
|
15
15
|
export const MAX_SAMPLE_FILE_BYTES = 512 * 1024;
|
|
16
16
|
/** Files the agent owns at runtime; preserved across sample upgrades. */
|
|
17
17
|
const USER_DATA_FILES = ["state.json", "events.jsonl"];
|
|
18
|
+
/**
|
|
19
|
+
* Windows refuses to unlink or rename a directory while ANY file inside it is
|
|
20
|
+
* still open, and reports it as a sharing violation (EPERM/EBUSY/EACCES) or a
|
|
21
|
+
* non-empty directory. A still-draining sample server, an antivirus scan or the
|
|
22
|
+
* search indexer is enough to trigger it, and all of those clear within
|
|
23
|
+
* milliseconds — so a short bounded retry turns a hard install failure into a
|
|
24
|
+
* brief pause. On POSIX these codes essentially never appear here and the first
|
|
25
|
+
* attempt always wins, so the retry costs nothing.
|
|
26
|
+
*/
|
|
27
|
+
const FS_SWAP_RETRY_DELAYS_MS = [50, 150, 400];
|
|
28
|
+
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY"]);
|
|
29
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
async function withFsSwapRetry(op) {
|
|
31
|
+
let lastError;
|
|
32
|
+
for (let attempt = 0; attempt <= FS_SWAP_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
33
|
+
try {
|
|
34
|
+
return await op();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
lastError = err;
|
|
38
|
+
const code = err.code ?? "";
|
|
39
|
+
const delay = FS_SWAP_RETRY_DELAYS_MS[attempt];
|
|
40
|
+
if (!TRANSIENT_FS_CODES.has(code) || delay === undefined)
|
|
41
|
+
break;
|
|
42
|
+
await sleep(delay);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw lastError;
|
|
46
|
+
}
|
|
18
47
|
function livewaresBase(base, ref) {
|
|
19
48
|
return `${(base ?? OFFICIAL_SKILLS_BASE).replace(/\/+$/, "")}/${ref}/livewares`;
|
|
20
49
|
}
|
|
@@ -116,6 +145,14 @@ export async function downloadLivewareSample(opts) {
|
|
|
116
145
|
if (await localInstallMatches(appDir, entry)) {
|
|
117
146
|
return { version: entry.version, appDir, reused: true };
|
|
118
147
|
}
|
|
148
|
+
// Sweep app.old-* left behind when a previous swap could not delete the old
|
|
149
|
+
// tree (a handle was still open then, but is not now). Best effort: a dir we
|
|
150
|
+
// still cannot remove is simply retried next run.
|
|
151
|
+
for (const name of await fs.promises.readdir(opts.sampleRoot).catch(() => [])) {
|
|
152
|
+
if (name.startsWith("app.old-")) {
|
|
153
|
+
await fs.promises.rm(path.join(opts.sampleRoot, name), { recursive: true, force: true }).catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
119
156
|
const tmpDir = path.join(opts.sampleRoot, `app.tmp-${process.pid}`);
|
|
120
157
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
121
158
|
await fs.promises.mkdir(tmpDir, { recursive: true });
|
|
@@ -138,8 +175,18 @@ export async function downloadLivewareSample(opts) {
|
|
|
138
175
|
await fs.promises.copyFile(prev, path.join(tmpDir, name));
|
|
139
176
|
}
|
|
140
177
|
}
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
// Swap by moving the old tree ASIDE rather than deleting it first. On
|
|
179
|
+
// Windows the delete is the step most likely to lose a race with an open
|
|
180
|
+
// handle, and doing it first leaves no app/ at all when it fails. Renaming
|
|
181
|
+
// aside puts the new tree in place first and makes the delete best-effort:
|
|
182
|
+
// a leftover app.old-<pid> is swept on the next run, never a broken install.
|
|
183
|
+
const oldDir = path.join(opts.sampleRoot, `app.old-${process.pid}`);
|
|
184
|
+
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => { });
|
|
185
|
+
if (fs.existsSync(appDir)) {
|
|
186
|
+
await withFsSwapRetry(() => fs.promises.rename(appDir, oldDir));
|
|
187
|
+
}
|
|
188
|
+
await withFsSwapRetry(() => fs.promises.rename(tmpDir, appDir));
|
|
189
|
+
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => { });
|
|
143
190
|
return { version: entry.version, appDir };
|
|
144
191
|
}
|
|
145
192
|
catch (err) {
|
|
@@ -147,6 +194,35 @@ export async function downloadLivewareSample(opts) {
|
|
|
147
194
|
throw err;
|
|
148
195
|
}
|
|
149
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Terminate a child and everything it spawned.
|
|
199
|
+
*
|
|
200
|
+
* POSIX: `SIGTERM` to the child is what we want and what the child expects.
|
|
201
|
+
*
|
|
202
|
+
* Windows: there are no signals. `child.kill()` maps to TerminateProcess on
|
|
203
|
+
* that one process, so any grandchild the liveware agent spawned survives —
|
|
204
|
+
* orphaned, still holding the sample port, and invisible to the next boot,
|
|
205
|
+
* which then fails to bind. `taskkill /T /F` takes the whole tree down.
|
|
206
|
+
* Fire-and-forget: we never wait on it, and a failure (already dead, no such
|
|
207
|
+
* pid) falls through to the ordinary kill().
|
|
208
|
+
*/
|
|
209
|
+
export function killProcessTree(child, execFileFn = nodeExecFile) {
|
|
210
|
+
if (!child)
|
|
211
|
+
return;
|
|
212
|
+
if (process.platform === "win32" && typeof child.pid === "number") {
|
|
213
|
+
try {
|
|
214
|
+
execFileFn("taskkill", ["/pid", String(child.pid), "/T", "/F"], () => { });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// taskkill missing or spawn refused — fall back to the plain kill below.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
child.kill("SIGTERM");
|
|
223
|
+
}
|
|
224
|
+
catch { /* already dead */ }
|
|
225
|
+
}
|
|
150
226
|
const SERVER_START_TIMEOUT_MS = 10_000;
|
|
151
227
|
const TUNNEL_START_TIMEOUT_MS = 30_000;
|
|
152
228
|
const CLI_TIMEOUT_MS = 30_000;
|
|
@@ -164,10 +240,7 @@ function waitForOutput(child, match, timeoutMs, label) {
|
|
|
164
240
|
child.stderr?.removeListener("data", onData);
|
|
165
241
|
child.removeListener("exit", onExit);
|
|
166
242
|
if (err) {
|
|
167
|
-
|
|
168
|
-
child.kill("SIGTERM");
|
|
169
|
-
}
|
|
170
|
-
catch { /* already dead */ }
|
|
243
|
+
killProcessTree(child);
|
|
171
244
|
reject(err);
|
|
172
245
|
}
|
|
173
246
|
else {
|
|
@@ -535,10 +608,7 @@ export class LivewareSampleSupervisor {
|
|
|
535
608
|
killChildren() {
|
|
536
609
|
this.generation += 1; // invalidate pending exit handlers before killing
|
|
537
610
|
for (const child of [this.serverChild, this.tunnelChild]) {
|
|
538
|
-
|
|
539
|
-
child?.kill("SIGTERM");
|
|
540
|
-
}
|
|
541
|
-
catch { /* already dead */ }
|
|
611
|
+
killProcessTree(child, this.deps.execFileFn);
|
|
542
612
|
}
|
|
543
613
|
this.serverChild = null;
|
|
544
614
|
this.tunnelChild = null;
|
package/dist/src/skill-update.js
CHANGED
|
@@ -38,9 +38,11 @@ import { fileURLToPath } from "node:url";
|
|
|
38
38
|
* - All network IO goes through an injected `FetchLike`; tests never touch the
|
|
39
39
|
* real network.
|
|
40
40
|
*
|
|
41
|
-
* Cross-language contract / spec: see
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* Cross-language contract / spec: see `docs/clawchat-plugin-openclaw.md`
|
|
42
|
+
* §"Conversational skill hot-update" for this adapter's end of the flow,
|
|
43
|
+
* `docs/client-integration.md` for the `clawchat.skill.update.check` signal
|
|
44
|
+
* shape, and the install-cli repo's `skills/README.md` for the canonical skill
|
|
45
|
+
* source and `manifest.json` contract.
|
|
44
46
|
*/
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
46
48
|
// Constants — fixed contract (must match the install-cli `config.ts`).
|
package/dist/src/tools-schema.js
CHANGED
|
@@ -128,6 +128,16 @@ export const ClawchatLeaveGroupSchema = Type.Object({
|
|
|
128
128
|
description: "Concrete ClawChat group conversation id to leave",
|
|
129
129
|
}),
|
|
130
130
|
});
|
|
131
|
+
export const ClawchatAddGroupMemberSchema = Type.Object({
|
|
132
|
+
conversationId: Type.String({
|
|
133
|
+
minLength: 1,
|
|
134
|
+
description: "Concrete ClawChat group conversation id to add the user to",
|
|
135
|
+
}),
|
|
136
|
+
userId: Type.String({
|
|
137
|
+
minLength: 1,
|
|
138
|
+
description: "Concrete ClawChat user id (usr_...) to add to the group",
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
131
141
|
export const ClawchatMentionMessageSchema = Type.Object({
|
|
132
142
|
chatId: Type.String({
|
|
133
143
|
minLength: 1,
|
package/dist/src/tools.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getOpenclawClawlingClient, } from "./runtime.js";
|
|
|
12
12
|
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
|
|
13
13
|
import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
|
|
14
14
|
import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
|
|
15
|
-
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
15
|
+
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAddGroupMemberSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
16
16
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
17
17
|
// Owner-approval gate business codes (must match the ClawChat backend's owner-approval codes).
|
|
18
18
|
const CODE_PENDING_APPROVAL = 21001;
|
|
@@ -808,6 +808,34 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
808
808
|
});
|
|
809
809
|
},
|
|
810
810
|
}, { name: "clawchat_leave_group" });
|
|
811
|
+
api.registerTool({
|
|
812
|
+
name: "clawchat_add_group_member",
|
|
813
|
+
label: "Add ClawChat Group Member",
|
|
814
|
+
description: toolDescription("Add a specified ClawChat user to a group conversation the agent is a member of. " +
|
|
815
|
+
"TRIGGER - invoke only when the user explicitly asks the agent to add, invite, or pull a specific person into a specific ClawChat group, and both a concrete conversationId and a concrete userId are available. " +
|
|
816
|
+
"Never guess a userId from a name or nickname — use ids from ClawChat Group Message Metadata, clawchat_list_account_friends, or clawchat_search_users. " +
|
|
817
|
+
"Groups only: direct conversations are rejected by the server. " +
|
|
818
|
+
"The target must already be the agent's ClawChat friend, and the agent needs the group-management permission granted by its owner; if the call fails for either reason, explain that to the user and do not retry."),
|
|
819
|
+
parameters: ClawchatAddGroupMemberSchema,
|
|
820
|
+
async execute(_callId, params) {
|
|
821
|
+
return await recordClawchatToolCall("clawchat_add_group_member", params, async () => {
|
|
822
|
+
const p = params;
|
|
823
|
+
const built = buildClient();
|
|
824
|
+
if (!built.ok)
|
|
825
|
+
return built.error;
|
|
826
|
+
try {
|
|
827
|
+
const data = await built.client.addConversationMember(p.conversationId, p.userId);
|
|
828
|
+
return jsonResponse(data);
|
|
829
|
+
}
|
|
830
|
+
catch (err) {
|
|
831
|
+
if (err instanceof ClawlingApiError) {
|
|
832
|
+
return apiError(err);
|
|
833
|
+
}
|
|
834
|
+
return genericError(err);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
}, { name: "clawchat_add_group_member" });
|
|
811
839
|
api.registerTool({
|
|
812
840
|
name: "clawchat_mention_message",
|
|
813
841
|
label: "Send ClawChat Mention Message",
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -135,6 +135,10 @@ export interface OpenclawClawlingApiClient {
|
|
|
135
135
|
deleteMomentComment(params: { momentId: number; commentId: number }): Promise<{ ok: boolean }>;
|
|
136
136
|
getConversation(conversationId: string): Promise<{ conversation: ConversationDetails }>;
|
|
137
137
|
leaveConversation(conversationId: string): Promise<{ ok: boolean }>;
|
|
138
|
+
addConversationMember(
|
|
139
|
+
conversationId: string,
|
|
140
|
+
userId: string,
|
|
141
|
+
): Promise<{ conversation: ConversationDetails }>;
|
|
138
142
|
patchConversation(
|
|
139
143
|
conversationId: string,
|
|
140
144
|
patch: ConversationMetadataPatch,
|
|
@@ -643,6 +647,23 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
643
647
|
`/v1/conversations/${encodeURIComponent(conversationId)}/leave`,
|
|
644
648
|
);
|
|
645
649
|
},
|
|
650
|
+
async addConversationMember(
|
|
651
|
+
conversationId,
|
|
652
|
+
userId,
|
|
653
|
+
): Promise<{ conversation: ConversationDetails }> {
|
|
654
|
+
assertNonBlankId(conversationId, "addConversationMember: conversationId");
|
|
655
|
+
assertNonBlankId(userId, "addConversationMember: userId");
|
|
656
|
+
const cid = conversationId.trim();
|
|
657
|
+
const uid = userId.trim();
|
|
658
|
+
return await call<{ conversation: ConversationDetails }>(
|
|
659
|
+
"POST",
|
|
660
|
+
`/v1/conversations/${encodeURIComponent(cid)}/members`,
|
|
661
|
+
{
|
|
662
|
+
body: JSON.stringify({ user_id: uid }),
|
|
663
|
+
headers: { "content-type": "application/json" },
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
},
|
|
646
667
|
async patchConversation(
|
|
647
668
|
conversationId: string,
|
|
648
669
|
patch: ConversationMetadataPatch,
|
package/src/liveware-sample.ts
CHANGED
|
@@ -20,6 +20,36 @@ export const MAX_SAMPLE_FILE_BYTES = 512 * 1024;
|
|
|
20
20
|
/** Files the agent owns at runtime; preserved across sample upgrades. */
|
|
21
21
|
const USER_DATA_FILES = ["state.json", "events.jsonl"];
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Windows refuses to unlink or rename a directory while ANY file inside it is
|
|
25
|
+
* still open, and reports it as a sharing violation (EPERM/EBUSY/EACCES) or a
|
|
26
|
+
* non-empty directory. A still-draining sample server, an antivirus scan or the
|
|
27
|
+
* search indexer is enough to trigger it, and all of those clear within
|
|
28
|
+
* milliseconds — so a short bounded retry turns a hard install failure into a
|
|
29
|
+
* brief pause. On POSIX these codes essentially never appear here and the first
|
|
30
|
+
* attempt always wins, so the retry costs nothing.
|
|
31
|
+
*/
|
|
32
|
+
const FS_SWAP_RETRY_DELAYS_MS = [50, 150, 400];
|
|
33
|
+
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY"]);
|
|
34
|
+
|
|
35
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
36
|
+
|
|
37
|
+
async function withFsSwapRetry<T>(op: () => Promise<T>): Promise<T> {
|
|
38
|
+
let lastError: unknown;
|
|
39
|
+
for (let attempt = 0; attempt <= FS_SWAP_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
40
|
+
try {
|
|
41
|
+
return await op();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
lastError = err;
|
|
44
|
+
const code = (err as NodeJS.ErrnoException).code ?? "";
|
|
45
|
+
const delay = FS_SWAP_RETRY_DELAYS_MS[attempt];
|
|
46
|
+
if (!TRANSIENT_FS_CODES.has(code) || delay === undefined) break;
|
|
47
|
+
await sleep(delay);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw lastError;
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
export interface LivewareSampleManifestFile {
|
|
24
54
|
path: string;
|
|
25
55
|
sha256: string;
|
|
@@ -151,6 +181,15 @@ export async function downloadLivewareSample(opts: {
|
|
|
151
181
|
if (await localInstallMatches(appDir, entry)) {
|
|
152
182
|
return { version: entry.version, appDir, reused: true };
|
|
153
183
|
}
|
|
184
|
+
// Sweep app.old-* left behind when a previous swap could not delete the old
|
|
185
|
+
// tree (a handle was still open then, but is not now). Best effort: a dir we
|
|
186
|
+
// still cannot remove is simply retried next run.
|
|
187
|
+
for (const name of await fs.promises.readdir(opts.sampleRoot).catch(() => [])) {
|
|
188
|
+
if (name.startsWith("app.old-")) {
|
|
189
|
+
await fs.promises.rm(path.join(opts.sampleRoot, name), { recursive: true, force: true }).catch(() => {});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
154
193
|
const tmpDir = path.join(opts.sampleRoot, `app.tmp-${process.pid}`);
|
|
155
194
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
156
195
|
await fs.promises.mkdir(tmpDir, { recursive: true });
|
|
@@ -173,8 +212,18 @@ export async function downloadLivewareSample(opts: {
|
|
|
173
212
|
await fs.promises.copyFile(prev, path.join(tmpDir, name));
|
|
174
213
|
}
|
|
175
214
|
}
|
|
176
|
-
|
|
177
|
-
|
|
215
|
+
// Swap by moving the old tree ASIDE rather than deleting it first. On
|
|
216
|
+
// Windows the delete is the step most likely to lose a race with an open
|
|
217
|
+
// handle, and doing it first leaves no app/ at all when it fails. Renaming
|
|
218
|
+
// aside puts the new tree in place first and makes the delete best-effort:
|
|
219
|
+
// a leftover app.old-<pid> is swept on the next run, never a broken install.
|
|
220
|
+
const oldDir = path.join(opts.sampleRoot, `app.old-${process.pid}`);
|
|
221
|
+
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => {});
|
|
222
|
+
if (fs.existsSync(appDir)) {
|
|
223
|
+
await withFsSwapRetry(() => fs.promises.rename(appDir, oldDir));
|
|
224
|
+
}
|
|
225
|
+
await withFsSwapRetry(() => fs.promises.rename(tmpDir, appDir));
|
|
226
|
+
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => {});
|
|
178
227
|
return { version: entry.version, appDir };
|
|
179
228
|
} catch (err) {
|
|
180
229
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
@@ -185,6 +234,34 @@ export async function downloadLivewareSample(opts: {
|
|
|
185
234
|
export type SpawnLike = typeof nodeSpawn;
|
|
186
235
|
export type ExecFileLike = typeof nodeExecFile;
|
|
187
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Terminate a child and everything it spawned.
|
|
239
|
+
*
|
|
240
|
+
* POSIX: `SIGTERM` to the child is what we want and what the child expects.
|
|
241
|
+
*
|
|
242
|
+
* Windows: there are no signals. `child.kill()` maps to TerminateProcess on
|
|
243
|
+
* that one process, so any grandchild the liveware agent spawned survives —
|
|
244
|
+
* orphaned, still holding the sample port, and invisible to the next boot,
|
|
245
|
+
* which then fails to bind. `taskkill /T /F` takes the whole tree down.
|
|
246
|
+
* Fire-and-forget: we never wait on it, and a failure (already dead, no such
|
|
247
|
+
* pid) falls through to the ordinary kill().
|
|
248
|
+
*/
|
|
249
|
+
export function killProcessTree(
|
|
250
|
+
child: ChildProcess | null | undefined,
|
|
251
|
+
execFileFn: ExecFileLike = nodeExecFile,
|
|
252
|
+
): void {
|
|
253
|
+
if (!child) return;
|
|
254
|
+
if (process.platform === "win32" && typeof child.pid === "number") {
|
|
255
|
+
try {
|
|
256
|
+
execFileFn("taskkill", ["/pid", String(child.pid), "/T", "/F"], () => {});
|
|
257
|
+
return;
|
|
258
|
+
} catch {
|
|
259
|
+
// taskkill missing or spawn refused — fall back to the plain kill below.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
try { child.kill("SIGTERM"); } catch { /* already dead */ }
|
|
263
|
+
}
|
|
264
|
+
|
|
188
265
|
const SERVER_START_TIMEOUT_MS = 10_000;
|
|
189
266
|
const TUNNEL_START_TIMEOUT_MS = 30_000;
|
|
190
267
|
const CLI_TIMEOUT_MS = 30_000;
|
|
@@ -207,7 +284,7 @@ function waitForOutput(
|
|
|
207
284
|
child.stderr?.removeListener("data", onData);
|
|
208
285
|
child.removeListener("exit", onExit);
|
|
209
286
|
if (err) {
|
|
210
|
-
|
|
287
|
+
killProcessTree(child);
|
|
211
288
|
reject(err);
|
|
212
289
|
} else {
|
|
213
290
|
resolve(value as string);
|
|
@@ -670,7 +747,7 @@ export class LivewareSampleSupervisor {
|
|
|
670
747
|
private killChildren(): void {
|
|
671
748
|
this.generation += 1; // invalidate pending exit handlers before killing
|
|
672
749
|
for (const child of [this.serverChild, this.tunnelChild]) {
|
|
673
|
-
|
|
750
|
+
killProcessTree(child, this.deps.execFileFn);
|
|
674
751
|
}
|
|
675
752
|
this.serverChild = null;
|
|
676
753
|
this.tunnelChild = null;
|
package/src/skill-update.ts
CHANGED
|
@@ -39,9 +39,11 @@ import { fileURLToPath } from "node:url";
|
|
|
39
39
|
* - All network IO goes through an injected `FetchLike`; tests never touch the
|
|
40
40
|
* real network.
|
|
41
41
|
*
|
|
42
|
-
* Cross-language contract / spec: see
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Cross-language contract / spec: see `docs/clawchat-plugin-openclaw.md`
|
|
43
|
+
* §"Conversational skill hot-update" for this adapter's end of the flow,
|
|
44
|
+
* `docs/client-integration.md` for the `clawchat.skill.update.check` signal
|
|
45
|
+
* shape, and the install-cli repo's `skills/README.md` for the canonical skill
|
|
46
|
+
* source and `manifest.json` contract.
|
|
45
47
|
*/
|
|
46
48
|
|
|
47
49
|
// ---------------------------------------------------------------------------
|
package/src/tools-schema.ts
CHANGED
|
@@ -208,6 +208,18 @@ export const ClawchatLeaveGroupSchema = Type.Object({
|
|
|
208
208
|
});
|
|
209
209
|
export type ClawchatLeaveGroupParams = Static<typeof ClawchatLeaveGroupSchema>;
|
|
210
210
|
|
|
211
|
+
export const ClawchatAddGroupMemberSchema = Type.Object({
|
|
212
|
+
conversationId: Type.String({
|
|
213
|
+
minLength: 1,
|
|
214
|
+
description: "Concrete ClawChat group conversation id to add the user to",
|
|
215
|
+
}),
|
|
216
|
+
userId: Type.String({
|
|
217
|
+
minLength: 1,
|
|
218
|
+
description: "Concrete ClawChat user id (usr_...) to add to the group",
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
export type ClawchatAddGroupMemberParams = Static<typeof ClawchatAddGroupMemberSchema>;
|
|
222
|
+
|
|
211
223
|
export const ClawchatMentionMessageSchema = Type.Object({
|
|
212
224
|
chatId: Type.String({
|
|
213
225
|
minLength: 1,
|
package/src/tools.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
ClawchatGetMomentSchema,
|
|
44
44
|
ClawchatGetUserProfileSchema,
|
|
45
45
|
ClawchatLeaveGroupSchema,
|
|
46
|
+
ClawchatAddGroupMemberSchema,
|
|
46
47
|
ClawchatAcceptFriendRequestSchema,
|
|
47
48
|
ClawchatMemoryEditSchema,
|
|
48
49
|
ClawchatMemoryReadSchema,
|
|
@@ -86,6 +87,7 @@ import {
|
|
|
86
87
|
type ClawchatGetMomentParams,
|
|
87
88
|
type ClawchatGetUserProfileParams,
|
|
88
89
|
type ClawchatLeaveGroupParams,
|
|
90
|
+
type ClawchatAddGroupMemberParams,
|
|
89
91
|
type ClawchatListFriendRequestsParams,
|
|
90
92
|
type ClawchatListMomentsParams,
|
|
91
93
|
type ClawchatMentionMessageParams,
|
|
@@ -1048,6 +1050,38 @@ export function registerOpenclawClawlingTools(
|
|
|
1048
1050
|
{ name: "clawchat_leave_group" },
|
|
1049
1051
|
);
|
|
1050
1052
|
|
|
1053
|
+
api.registerTool(
|
|
1054
|
+
{
|
|
1055
|
+
name: "clawchat_add_group_member",
|
|
1056
|
+
label: "Add ClawChat Group Member",
|
|
1057
|
+
description: toolDescription(
|
|
1058
|
+
"Add a specified ClawChat user to a group conversation the agent is a member of. " +
|
|
1059
|
+
"TRIGGER - invoke only when the user explicitly asks the agent to add, invite, or pull a specific person into a specific ClawChat group, and both a concrete conversationId and a concrete userId are available. " +
|
|
1060
|
+
"Never guess a userId from a name or nickname — use ids from ClawChat Group Message Metadata, clawchat_list_account_friends, or clawchat_search_users. " +
|
|
1061
|
+
"Groups only: direct conversations are rejected by the server. " +
|
|
1062
|
+
"The target must already be the agent's ClawChat friend, and the agent needs the group-management permission granted by its owner; if the call fails for either reason, explain that to the user and do not retry.",
|
|
1063
|
+
),
|
|
1064
|
+
parameters: ClawchatAddGroupMemberSchema,
|
|
1065
|
+
async execute(_callId, params) {
|
|
1066
|
+
return await recordClawchatToolCall("clawchat_add_group_member", params, async () => {
|
|
1067
|
+
const p = params as ClawchatAddGroupMemberParams;
|
|
1068
|
+
const built = buildClient();
|
|
1069
|
+
if (!built.ok) return built.error;
|
|
1070
|
+
try {
|
|
1071
|
+
const data = await built.client.addConversationMember(p.conversationId, p.userId);
|
|
1072
|
+
return jsonResponse(data);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
if (err instanceof ClawlingApiError) {
|
|
1075
|
+
return apiError(err);
|
|
1076
|
+
}
|
|
1077
|
+
return genericError(err);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
{ name: "clawchat_add_group_member" },
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1051
1085
|
api.registerTool(
|
|
1052
1086
|
{
|
|
1053
1087
|
name: "clawchat_mention_message",
|