@clawling/clawchat-plugin-openclaw 2026.6.30-4 → 2026.7.2-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 +13 -0
- package/dist/src/config.js +3 -0
- package/dist/src/friend-request-turn.js +73 -0
- package/dist/src/gate-outcome.js +26 -0
- package/dist/src/inbound.js +11 -0
- package/dist/src/permission-result.js +107 -0
- package/dist/src/permissions.js +6 -0
- package/dist/src/runtime.js +131 -9
- package/dist/src/skill-update.js +139 -69
- package/dist/src/tools.js +43 -10
- package/dist/src/ws-client.js +19 -6
- package/openclaw.plugin.json +2 -0
- package/package.json +1 -1
- package/src/api-client.ts +23 -0
- package/src/api-types.ts +1 -1
- package/src/config.ts +7 -0
- package/src/friend-request-turn.ts +87 -0
- package/src/gate-outcome.ts +44 -0
- package/src/inbound.ts +28 -0
- package/src/permission-result.ts +128 -0
- package/src/permissions.ts +13 -0
- package/src/runtime.ts +146 -10
- package/src/skill-update.ts +170 -85
- package/src/tools.ts +44 -10
- package/src/ws-client.ts +21 -6
package/dist/src/skill-update.js
CHANGED
|
@@ -29,8 +29,12 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
* implementation in `@clawling/clawchat-plugin-install-cli`
|
|
30
30
|
* (`packages/core/src/skills/check-update.ts`). That package is
|
|
31
31
|
* workspace-private, so the logic is duplicated here rather than imported.
|
|
32
|
-
* -
|
|
33
|
-
* (
|
|
32
|
+
* - Update detection converges on raw sha256 comparison: ANY byte difference
|
|
33
|
+
* from the official source (or a missing local file) is an update —
|
|
34
|
+
* upgrade, rollback, or same-version content fix alike. The strict semver
|
|
35
|
+
* compare (`parseComparableVersion` / `isVersionOlder`, still ported from
|
|
36
|
+
* the reference `parseComparableVersion`, `X.Y[.Z[.W]][-buildnum]`) is kept
|
|
37
|
+
* only for consent-prompt display wording, not for the `hasUpdate` decision.
|
|
34
38
|
* - All network IO goes through an injected `FetchLike`; tests never touch the
|
|
35
39
|
* real network.
|
|
36
40
|
*
|
|
@@ -151,7 +155,25 @@ export function parseSkillsManifest(text) {
|
|
|
151
155
|
skills[target][skillId] = asEntry(entry, `${target}.${skillId}`);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
|
-
|
|
158
|
+
const removed = {};
|
|
159
|
+
if (data.removed !== undefined) {
|
|
160
|
+
if (!data.removed || typeof data.removed !== "object" || Array.isArray(data.removed)) {
|
|
161
|
+
throw new Error("skills manifest `removed` must be an object");
|
|
162
|
+
}
|
|
163
|
+
for (const [target, ids] of Object.entries(data.removed)) {
|
|
164
|
+
if (!Array.isArray(ids) || !ids.every((i) => typeof i === "string" && i.trim())) {
|
|
165
|
+
throw new Error(`skills manifest removed[${target}] must be a list of skill ids`);
|
|
166
|
+
}
|
|
167
|
+
const cleaned = ids.map((i) => i.trim());
|
|
168
|
+
for (const id of cleaned) {
|
|
169
|
+
if (skills[target]?.[id]) {
|
|
170
|
+
throw new Error(`skill ${target}.${id} is in both skills and removed`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
removed[target] = cleaned;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { schema: 1, skills, removed };
|
|
155
177
|
}
|
|
156
178
|
async function fetchText(url, fetchFn) {
|
|
157
179
|
let response;
|
|
@@ -167,9 +189,10 @@ async function fetchText(url, fetchFn) {
|
|
|
167
189
|
return response.text();
|
|
168
190
|
}
|
|
169
191
|
/**
|
|
170
|
-
* Read the official manifest for the `openclaw` target and compare each
|
|
171
|
-
* offered
|
|
172
|
-
*
|
|
192
|
+
* Read the official manifest for the `openclaw` target and compare each
|
|
193
|
+
* skill's offered content (sha256) against its effective local state, read
|
|
194
|
+
* on demand via `readLocalState`. Any sha256 mismatch — or no local state at
|
|
195
|
+
* all — is reported as `hasUpdate: true`.
|
|
173
196
|
*/
|
|
174
197
|
export async function checkSkillUpdate(options) {
|
|
175
198
|
const ref = options.ref ?? DEFAULT_SKILLS_REF;
|
|
@@ -181,11 +204,14 @@ export async function checkSkillUpdate(options) {
|
|
|
181
204
|
}
|
|
182
205
|
const results = [];
|
|
183
206
|
for (const [skillId, entry] of Object.entries(targetSkills)) {
|
|
184
|
-
const
|
|
185
|
-
|
|
207
|
+
const state = await options.readLocalState(skillId);
|
|
208
|
+
// sha convergence: any byte difference from the official source (or a
|
|
209
|
+
// missing local file) is an update — upgrade, rollback, or same-version
|
|
210
|
+
// content fix alike.
|
|
211
|
+
const hasUpdate = !state || state.sha256 !== entry.sha256;
|
|
186
212
|
results.push({
|
|
187
213
|
skillId,
|
|
188
|
-
current,
|
|
214
|
+
current: state?.version ?? null,
|
|
189
215
|
latest: entry.version,
|
|
190
216
|
hasUpdate,
|
|
191
217
|
path: entry.path,
|
|
@@ -193,7 +219,12 @@ export async function checkSkillUpdate(options) {
|
|
|
193
219
|
bytes: entry.bytes,
|
|
194
220
|
});
|
|
195
221
|
}
|
|
196
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ref,
|
|
224
|
+
results,
|
|
225
|
+
hasUpdate: results.some((r) => r.hasUpdate),
|
|
226
|
+
removedIds: manifest.removed[SKILL_TARGET] ?? [],
|
|
227
|
+
};
|
|
197
228
|
}
|
|
198
229
|
/**
|
|
199
230
|
* Download one skill markdown file and integrity-check it against the manifest
|
|
@@ -230,52 +261,41 @@ export function parseSkillFrontmatterVersion(markdown) {
|
|
|
230
261
|
const raw = (versionLine[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
231
262
|
return raw || null;
|
|
232
263
|
}
|
|
233
|
-
|
|
234
|
-
async function readSingleSkillVersion(dir, skillId) {
|
|
264
|
+
async function readSkillStateFromDir(dir, skillId) {
|
|
235
265
|
try {
|
|
236
|
-
const
|
|
237
|
-
return
|
|
266
|
+
const buf = await fs.readFile(path.join(dir, skillId, "SKILL.md"));
|
|
267
|
+
return {
|
|
268
|
+
version: parseSkillFrontmatterVersion(buf.toString("utf8")),
|
|
269
|
+
sha256: crypto.createHash("sha256").update(buf).digest("hex"),
|
|
270
|
+
};
|
|
238
271
|
}
|
|
239
272
|
catch {
|
|
240
|
-
return null;
|
|
273
|
+
return null;
|
|
241
274
|
}
|
|
242
275
|
}
|
|
243
276
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
277
|
+
* Effective local state of ONE skill, mirroring the host's managed-over-bundled
|
|
278
|
+
* precedence: managed copy if present, else bundled, else null. Called per
|
|
279
|
+
* remote manifest entry so dynamically delivered extra skills are hashed too
|
|
280
|
+
* (a static bundled-id list would misreport extras as never-installed).
|
|
247
281
|
*/
|
|
248
|
-
export async function
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return versions;
|
|
282
|
+
export async function readLocalSkillState(managedDir, bundledDir, skillId) {
|
|
283
|
+
const managed = await readSkillStateFromDir(managedDir, skillId);
|
|
284
|
+
if (managed)
|
|
285
|
+
return managed;
|
|
286
|
+
if (bundledDir)
|
|
287
|
+
return readSkillStateFromDir(bundledDir, skillId);
|
|
288
|
+
return null;
|
|
256
289
|
}
|
|
257
|
-
/**
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const versions = {};
|
|
266
|
-
for (const skillId of skillIds) {
|
|
267
|
-
const managed = await readSingleSkillVersion(managedDir, skillId);
|
|
268
|
-
if (managed) {
|
|
269
|
-
versions[skillId] = managed;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (bundledDir) {
|
|
273
|
-
const bundled = await readSingleSkillVersion(bundledDir, skillId);
|
|
274
|
-
if (bundled)
|
|
275
|
-
versions[skillId] = bundled;
|
|
276
|
-
}
|
|
290
|
+
/** True when `<managedDir>/<skillId>/SKILL.md` exists (removal precondition). */
|
|
291
|
+
export async function managedSkillExists(managedDir, skillId) {
|
|
292
|
+
try {
|
|
293
|
+
await fs.access(path.join(managedDir, skillId, "SKILL.md"));
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
277
298
|
}
|
|
278
|
-
return versions;
|
|
279
299
|
}
|
|
280
300
|
/**
|
|
281
301
|
* Resolve OpenClaw's state directory, mirroring the host `src/utils.ts:132-154`:
|
|
@@ -359,6 +379,18 @@ export async function atomicWriteSkill(skillsDir, skillId, content) {
|
|
|
359
379
|
throw err;
|
|
360
380
|
}
|
|
361
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Delete a tombstoned managed skill: remove `<skillsDir>/<skillId>/SKILL.md`
|
|
384
|
+
* (the host chokidar-watches the dir; the unlink triggers a snapshot rebuild
|
|
385
|
+
* that drops the registration) and best-effort remove the now-empty dir.
|
|
386
|
+
* Deletion is the ONE legitimate delete of a skill path, and ONLY for ids the
|
|
387
|
+
* official manifest explicitly tombstones. Idempotent.
|
|
388
|
+
*/
|
|
389
|
+
export async function removeManagedSkill(skillsDir, skillId) {
|
|
390
|
+
const dir = path.join(skillsDir, skillId);
|
|
391
|
+
await fs.rm(path.join(dir, "SKILL.md"), { force: true });
|
|
392
|
+
await fs.rmdir(dir).catch(() => { });
|
|
393
|
+
}
|
|
362
394
|
const AFFIRM_TOKENS = new Set([
|
|
363
395
|
"更新",
|
|
364
396
|
"更新吧",
|
|
@@ -438,7 +470,20 @@ export class PendingConsentStore {
|
|
|
438
470
|
}
|
|
439
471
|
}
|
|
440
472
|
function describeUpdate(u) {
|
|
441
|
-
|
|
473
|
+
if (u.current === null)
|
|
474
|
+
return `「${u.skillId}」v无 → v${u.target}`;
|
|
475
|
+
let cmp = -1;
|
|
476
|
+
try {
|
|
477
|
+
cmp = compareVersions(u.current, u.target);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
cmp = -1; // unparsable local version — show as a plain update
|
|
481
|
+
}
|
|
482
|
+
if (cmp === 0)
|
|
483
|
+
return `「${u.skillId}」v${u.target}(内容修订)`;
|
|
484
|
+
if (cmp > 0)
|
|
485
|
+
return `「${u.skillId}」v${u.current} → v${u.target}(回滚)`;
|
|
486
|
+
return `「${u.skillId}」v${u.current} → v${u.target}`;
|
|
442
487
|
}
|
|
443
488
|
/**
|
|
444
489
|
* Step ③–⑤ (plan §2): on `clawchat.skill.update.check`, check the official
|
|
@@ -453,23 +498,25 @@ export async function runSkillUpdateCheck(options) {
|
|
|
453
498
|
return null;
|
|
454
499
|
}
|
|
455
500
|
const outcome = await checkSkillUpdate({
|
|
456
|
-
|
|
501
|
+
readLocalState: options.readLocalState,
|
|
457
502
|
...(options.ref ? { ref: options.ref } : {}),
|
|
458
503
|
...(options.base ? { base: options.base } : {}),
|
|
459
504
|
fetchFn: options.fetchFn,
|
|
460
505
|
});
|
|
461
506
|
const updates = outcome.results
|
|
462
507
|
.filter((r) => r.hasUpdate)
|
|
463
|
-
.map((r) => ({
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
508
|
+
.map((r) => ({ skillId: r.skillId, current: r.current, target: r.latest, path: r.path, sha256: r.sha256, bytes: r.bytes }));
|
|
509
|
+
const removals = [];
|
|
510
|
+
for (const skillId of outcome.removedIds) {
|
|
511
|
+
if (OPENCLAW_SKILL_IDS.includes(skillId)) {
|
|
512
|
+
options.log?.info?.(`clawchat skill-update: ignoring tombstone for bundled skill ${skillId}`);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (await options.hasManagedSkill(skillId))
|
|
516
|
+
removals.push({ skillId });
|
|
517
|
+
}
|
|
518
|
+
if (updates.length === 0 && removals.length === 0) {
|
|
519
|
+
options.log?.info?.("clawchat skill-update check: nothing to converge");
|
|
473
520
|
return null;
|
|
474
521
|
}
|
|
475
522
|
const now = (options.now ?? Date.now)();
|
|
@@ -478,13 +525,19 @@ export async function runSkillUpdateCheck(options) {
|
|
|
478
525
|
ref: outcome.ref,
|
|
479
526
|
...(options.base ? { base: options.base } : {}),
|
|
480
527
|
updates,
|
|
528
|
+
removals,
|
|
481
529
|
createdAt: now,
|
|
482
530
|
expiresAt: now + (options.timeoutMs ?? DEFAULT_CONSENT_TIMEOUT_MS),
|
|
483
531
|
};
|
|
484
532
|
options.store.set(record);
|
|
485
|
-
const
|
|
533
|
+
const segments = [];
|
|
534
|
+
if (updates.length > 0)
|
|
535
|
+
segments.push(`我的技能有更新:${updates.map(describeUpdate).join(";")}`);
|
|
536
|
+
if (removals.length > 0)
|
|
537
|
+
segments.push(`以下技能将下线移除:${removals.map((r) => `「${r.skillId}」`).join("、")}`);
|
|
538
|
+
const text = `${segments.join(";")}。回复「更新」确认,「取消」忽略。`;
|
|
486
539
|
await options.sendOwnerMessage(text);
|
|
487
|
-
options.log?.info?.(`clawchat skill-update check: asked owner for consent on ${updates.map((u) => u.skillId).join(",")}`);
|
|
540
|
+
options.log?.info?.(`clawchat skill-update check: asked owner for consent on updates=[${updates.map((u) => u.skillId).join(",")}] removals=[${removals.map((r) => r.skillId).join(",")}]`);
|
|
488
541
|
return record;
|
|
489
542
|
}
|
|
490
543
|
/**
|
|
@@ -516,22 +569,23 @@ export async function handleOwnerConsentReply(options) {
|
|
|
516
569
|
return true;
|
|
517
570
|
}
|
|
518
571
|
// verdict === "affirm"
|
|
519
|
-
const
|
|
572
|
+
const readLocalSha = options.readLocalSha ??
|
|
520
573
|
(async (skillId) => {
|
|
521
574
|
try {
|
|
522
|
-
const
|
|
523
|
-
return
|
|
575
|
+
const buf = await fs.readFile(path.join(options.skillsDir, skillId, "SKILL.md"));
|
|
576
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
524
577
|
}
|
|
525
578
|
catch {
|
|
526
579
|
return null;
|
|
527
580
|
}
|
|
528
581
|
});
|
|
529
582
|
const applied = [];
|
|
583
|
+
const removed = [];
|
|
530
584
|
try {
|
|
531
585
|
for (const update of pending.updates) {
|
|
532
|
-
// Idempotency: skip a skill already
|
|
533
|
-
const onDisk = await
|
|
534
|
-
if (onDisk === update.
|
|
586
|
+
// Idempotency: skip a skill whose on-disk bytes already match the target.
|
|
587
|
+
const onDisk = await readLocalSha(update.skillId);
|
|
588
|
+
if (onDisk === update.sha256) {
|
|
535
589
|
applied.push(update);
|
|
536
590
|
continue;
|
|
537
591
|
}
|
|
@@ -544,6 +598,18 @@ export async function handleOwnerConsentReply(options) {
|
|
|
544
598
|
applied.push(update);
|
|
545
599
|
options.log?.info?.(`clawchat skill-update: applied ${update.skillId} -> v${update.target} (atomic overwrite)`);
|
|
546
600
|
}
|
|
601
|
+
for (const removal of pending.removals) {
|
|
602
|
+
// Defensive re-guard: bundled skills must never be deleted, even though
|
|
603
|
+
// runSkillUpdateCheck already filters them out of removal candidates.
|
|
604
|
+
// Unreachable in the normal flow; kept as a cheap safety net.
|
|
605
|
+
if (OPENCLAW_SKILL_IDS.includes(removal.skillId)) {
|
|
606
|
+
options.log?.info?.(`clawchat skill-update: skipped removal of bundled skill ${removal.skillId} (defensive re-guard)`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
await removeManagedSkill(options.skillsDir, removal.skillId);
|
|
610
|
+
removed.push(removal);
|
|
611
|
+
options.log?.info?.(`clawchat skill-update: removed tombstoned skill ${removal.skillId}`);
|
|
612
|
+
}
|
|
547
613
|
}
|
|
548
614
|
catch (err) {
|
|
549
615
|
// Keep the pending record so the owner can retry with another "更新"; report
|
|
@@ -553,7 +619,11 @@ export async function handleOwnerConsentReply(options) {
|
|
|
553
619
|
return true;
|
|
554
620
|
}
|
|
555
621
|
options.store.clear();
|
|
556
|
-
const
|
|
557
|
-
|
|
622
|
+
const segments = [];
|
|
623
|
+
if (applied.length > 0)
|
|
624
|
+
segments.push(`已更新到 ${applied.map((u) => `「${u.skillId}」v${u.target}`).join("、")}`);
|
|
625
|
+
if (removed.length > 0)
|
|
626
|
+
segments.push(`已移除 ${removed.map((r) => `「${r.skillId}」`).join("、")}`);
|
|
627
|
+
await options.sendOwnerMessage(`✅ ${segments.join(";")}`);
|
|
558
628
|
return true;
|
|
559
629
|
}
|
package/dist/src/tools.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execFile } from "node:child_process";
|
|
|
4
4
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
5
5
|
import { resolveLivewarePath } from "./liveware-cli.js";
|
|
6
6
|
import { ClawlingApiError, } from "./api-types.js";
|
|
7
|
+
import { mapGateOutcome } from "./gate-outcome.js";
|
|
7
8
|
import { resolveOpenclawClawlingAccount } from "./config.js";
|
|
8
9
|
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
9
10
|
import { sendOpenclawClawlingMentionMessage } from "./outbound.js";
|
|
@@ -272,6 +273,38 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
272
273
|
}
|
|
273
274
|
})();
|
|
274
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Like withClient, but maps gate codes (21001/21003) to structured outcomes
|
|
278
|
+
* via mapGateOutcome so the agent sees a clear pending/forbidden status
|
|
279
|
+
* instead of a generic error. Used for friend and moment mutation tools.
|
|
280
|
+
*/
|
|
281
|
+
function withGatedClient(fn) {
|
|
282
|
+
return (async () => {
|
|
283
|
+
const built = buildClient();
|
|
284
|
+
if (!built.ok)
|
|
285
|
+
return built.error;
|
|
286
|
+
try {
|
|
287
|
+
const data = await fn(built.client);
|
|
288
|
+
return jsonResponse(data);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (err instanceof ClawlingApiError) {
|
|
292
|
+
const code = typeof err.meta?.code === "number" ? err.meta.code : null;
|
|
293
|
+
if (code !== null) {
|
|
294
|
+
const rawData = err.meta?.data;
|
|
295
|
+
const gateData = rawData !== null && rawData !== undefined && typeof rawData === "object"
|
|
296
|
+
? rawData
|
|
297
|
+
: {};
|
|
298
|
+
const outcome = mapGateOutcome(code, gateData);
|
|
299
|
+
if (outcome !== null)
|
|
300
|
+
return jsonResponse(outcome);
|
|
301
|
+
}
|
|
302
|
+
return apiError(err);
|
|
303
|
+
}
|
|
304
|
+
return genericError(err);
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
}
|
|
275
308
|
function workspaceRoot(ctx) {
|
|
276
309
|
const root = typeof ctx.workspaceDir === "string" ? ctx.workspaceDir.trim() : "";
|
|
277
310
|
return root || null;
|
|
@@ -640,7 +673,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
640
673
|
async execute(_callId, params) {
|
|
641
674
|
return await recordClawchatToolCall("clawchat_send_friend_request", params, async () => {
|
|
642
675
|
const p = params;
|
|
643
|
-
return await
|
|
676
|
+
return await withGatedClient((c) => c.sendFriendRequest(p));
|
|
644
677
|
});
|
|
645
678
|
},
|
|
646
679
|
}, { name: "clawchat_send_friend_request" });
|
|
@@ -668,7 +701,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
668
701
|
async execute(_callId, params) {
|
|
669
702
|
return await recordClawchatToolCall("clawchat_accept_friend_request", params, async () => {
|
|
670
703
|
const p = params;
|
|
671
|
-
return await
|
|
704
|
+
return await withGatedClient((c) => c.acceptFriendRequest(p.requestId));
|
|
672
705
|
});
|
|
673
706
|
},
|
|
674
707
|
}, { name: "clawchat_accept_friend_request" });
|
|
@@ -682,7 +715,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
682
715
|
async execute(_callId, params) {
|
|
683
716
|
return await recordClawchatToolCall("clawchat_reject_friend_request", params, async () => {
|
|
684
717
|
const p = params;
|
|
685
|
-
return await
|
|
718
|
+
return await withGatedClient((c) => c.rejectFriendRequest(p.requestId));
|
|
686
719
|
});
|
|
687
720
|
},
|
|
688
721
|
}, { name: "clawchat_reject_friend_request" });
|
|
@@ -696,7 +729,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
696
729
|
async execute(_callId, params) {
|
|
697
730
|
return await recordClawchatToolCall("clawchat_remove_friend", params, async () => {
|
|
698
731
|
const p = params;
|
|
699
|
-
return await
|
|
732
|
+
return await withGatedClient((c) => c.removeFriend(p.friendUserId));
|
|
700
733
|
});
|
|
701
734
|
},
|
|
702
735
|
}, { name: "clawchat_remove_friend" });
|
|
@@ -841,7 +874,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
841
874
|
if (!text && (!images || images.length === 0)) {
|
|
842
875
|
return validationError("clawchat-plugin-openclaw: at least one of text or images is required");
|
|
843
876
|
}
|
|
844
|
-
return await
|
|
877
|
+
return await withGatedClient((c) => c.createMoment({
|
|
845
878
|
...(text !== undefined ? { text } : {}),
|
|
846
879
|
...(images !== undefined ? { images } : {}),
|
|
847
880
|
}));
|
|
@@ -858,7 +891,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
858
891
|
async execute(_callId, params) {
|
|
859
892
|
return await recordClawchatToolCall("clawchat_delete_moment", params, async () => {
|
|
860
893
|
const p = params;
|
|
861
|
-
return await
|
|
894
|
+
return await withGatedClient((c) => c.deleteMoment(p.momentId));
|
|
862
895
|
});
|
|
863
896
|
},
|
|
864
897
|
}, { name: "clawchat_delete_moment" });
|
|
@@ -875,7 +908,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
875
908
|
if (!p.emoji?.trim()) {
|
|
876
909
|
return validationError("clawchat-plugin-openclaw: emoji is required");
|
|
877
910
|
}
|
|
878
|
-
return await
|
|
911
|
+
return await withGatedClient((c) => c.toggleMomentReaction({ momentId: p.momentId, emoji: p.emoji }));
|
|
879
912
|
});
|
|
880
913
|
},
|
|
881
914
|
}, { name: "clawchat_toggle_moment_reaction" });
|
|
@@ -892,7 +925,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
892
925
|
if (!p.text?.trim()) {
|
|
893
926
|
return validationError("clawchat-plugin-openclaw: text is required");
|
|
894
927
|
}
|
|
895
|
-
return await
|
|
928
|
+
return await withGatedClient((c) => c.createMomentComment({ momentId: p.momentId, text: p.text }));
|
|
896
929
|
});
|
|
897
930
|
},
|
|
898
931
|
}, { name: "clawchat_create_moment_comment" });
|
|
@@ -909,7 +942,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
909
942
|
if (!p.text?.trim()) {
|
|
910
943
|
return validationError("clawchat-plugin-openclaw: text is required");
|
|
911
944
|
}
|
|
912
|
-
return await
|
|
945
|
+
return await withGatedClient((c) => c.replyMomentComment({
|
|
913
946
|
momentId: p.momentId,
|
|
914
947
|
replyToCommentId: p.replyToCommentId,
|
|
915
948
|
text: p.text,
|
|
@@ -927,7 +960,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
927
960
|
async execute(_callId, params) {
|
|
928
961
|
return await recordClawchatToolCall("clawchat_delete_moment_comment", params, async () => {
|
|
929
962
|
const p = params;
|
|
930
|
-
return await
|
|
963
|
+
return await withGatedClient((c) => c.deleteMomentComment({ momentId: p.momentId, commentId: p.commentId }));
|
|
931
964
|
});
|
|
932
965
|
},
|
|
933
966
|
}, { name: "clawchat_delete_moment_comment" });
|
package/dist/src/ws-client.js
CHANGED
|
@@ -84,6 +84,23 @@ export function createWebSocketTransport(WebSocketCtor = globalThis.WebSocket) {
|
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the capability set advertised in the connect handshake.
|
|
89
|
+
* Exported so tests can assert on the exact fields without a full WS setup.
|
|
90
|
+
*
|
|
91
|
+
* permission_events: forward-compat: advertises readiness for a future native
|
|
92
|
+
* `permission.receipt` wire frame; today the permission-decision receipt is
|
|
93
|
+
* delivered as a system message until msghub emits the native frame.
|
|
94
|
+
*/
|
|
95
|
+
export function buildConnectCapabilities() {
|
|
96
|
+
return {
|
|
97
|
+
multi_device: false,
|
|
98
|
+
device_replay: true,
|
|
99
|
+
chat_meta_events: true,
|
|
100
|
+
notify_signals: true,
|
|
101
|
+
permission_events: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
87
104
|
export class ClawChatClient extends EventEmitter {
|
|
88
105
|
opts;
|
|
89
106
|
currentState = "idle";
|
|
@@ -333,12 +350,8 @@ export class ClawChatClient extends EventEmitter {
|
|
|
333
350
|
// *send* history.transit, which a single-device agent never does, and we
|
|
334
351
|
// do not handle inbound history.transit — advertising it would invite
|
|
335
352
|
// frames we cannot process. This is a deliberate, spec-legal omission.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
device_replay: true,
|
|
339
|
-
chat_meta_events: true,
|
|
340
|
-
notify_signals: true,
|
|
341
|
-
},
|
|
353
|
+
// permission_events: forward-compat; see buildConnectCapabilities().
|
|
354
|
+
capabilities: buildConnectCapabilities(),
|
|
342
355
|
};
|
|
343
356
|
const traceId = this.nextTraceId();
|
|
344
357
|
this.expectedConnectTraceId = traceId;
|
package/openclaw.plugin.json
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"forwardThinking": { "type": "boolean" },
|
|
108
108
|
"forwardToolCalls": { "type": "boolean" },
|
|
109
109
|
"richInteractions": { "type": "boolean" },
|
|
110
|
+
"awarenessNote": { "type": "boolean" },
|
|
110
111
|
"reconnect": {
|
|
111
112
|
"type": "object",
|
|
112
113
|
"additionalProperties": false,
|
|
@@ -180,6 +181,7 @@
|
|
|
180
181
|
"forwardThinking": { "type": "boolean" },
|
|
181
182
|
"forwardToolCalls": { "type": "boolean" },
|
|
182
183
|
"richInteractions": { "type": "boolean" },
|
|
184
|
+
"awarenessNote": { "type": "boolean" },
|
|
183
185
|
"reconnect": {
|
|
184
186
|
"type": "object",
|
|
185
187
|
"additionalProperties": false,
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type UserSearchHit,
|
|
18
18
|
} from "./api-types.ts";
|
|
19
19
|
import type { GroupSettings, GroupSettingsFetchResult } from "./group-settings.ts";
|
|
20
|
+
import type { PermissionPolicy, PermState } from "./permissions.ts";
|
|
20
21
|
import { CHANNEL_ID } from "./config.ts";
|
|
21
22
|
|
|
22
23
|
export interface PluginReportInput {
|
|
@@ -179,6 +180,17 @@ export interface OpenclawClawlingApiClient {
|
|
|
179
180
|
* error still throws.
|
|
180
181
|
*/
|
|
181
182
|
getMyGroupSettings(): Promise<GroupSettingsFetchResult>;
|
|
183
|
+
/**
|
|
184
|
+
* Fetch the agent's permission policy from `GET /v1/agents/me/permissions`.
|
|
185
|
+
*
|
|
186
|
+
* The response `data` is a flat object mapping operation name → state
|
|
187
|
+
* (`"ask"|"allow"|"deny"`), with one special key `"moment.visibility"` whose
|
|
188
|
+
* value is a string array. Returns a `PermissionPolicy` with `byOperation`
|
|
189
|
+
* (all non-visibility keys) and `momentVisibility` (the array). Throws on
|
|
190
|
+
* network errors or non-2xx responses; callers must catch and keep the stale
|
|
191
|
+
* cache (never throw / never block message handling).
|
|
192
|
+
*/
|
|
193
|
+
getMyPermissions(): Promise<PermissionPolicy>;
|
|
182
194
|
registerApp(params: { name: string; appId: string; url: string }): Promise<{ app: AgentAppView }>;
|
|
183
195
|
listApps(): Promise<{ apps: AgentAppView[] }>;
|
|
184
196
|
unregisterApp(appId: string): Promise<{ deleted: boolean }>;
|
|
@@ -362,6 +374,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
362
374
|
code,
|
|
363
375
|
status: res.status,
|
|
364
376
|
path,
|
|
377
|
+
...(env.data !== undefined ? { data: env.data } : {}),
|
|
365
378
|
});
|
|
366
379
|
}
|
|
367
380
|
return env.data as T;
|
|
@@ -759,5 +772,15 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
|
|
|
759
772
|
}
|
|
760
773
|
return { authoritative: true, rows };
|
|
761
774
|
},
|
|
775
|
+
async getMyPermissions(): Promise<PermissionPolicy> {
|
|
776
|
+
const data = await call<Record<string, unknown>>("GET", "/v1/agents/me/permissions");
|
|
777
|
+
const byOperation: Record<string, PermState> = {};
|
|
778
|
+
let momentVisibility: string[] = [];
|
|
779
|
+
for (const [k, v] of Object.entries(data ?? {})) {
|
|
780
|
+
if (k === "moment.visibility") momentVisibility = (v as string[]) ?? [];
|
|
781
|
+
else byOperation[k] = v as PermState;
|
|
782
|
+
}
|
|
783
|
+
return { byOperation, momentVisibility };
|
|
784
|
+
},
|
|
762
785
|
};
|
|
763
786
|
}
|
package/src/api-types.ts
CHANGED
|
@@ -157,7 +157,7 @@ export class ClawlingApiError extends Error {
|
|
|
157
157
|
constructor(
|
|
158
158
|
public readonly kind: ClawlingApiErrorKind,
|
|
159
159
|
message: string,
|
|
160
|
-
public readonly meta?: { code?: number; status?: number; path?: string },
|
|
160
|
+
public readonly meta?: { code?: number; status?: number; path?: string; data?: unknown },
|
|
161
161
|
) {
|
|
162
162
|
super(message);
|
|
163
163
|
this.name = "ClawlingApiError";
|
package/src/config.ts
CHANGED
|
@@ -106,6 +106,8 @@ export type OpenclawClawlingConfig = {
|
|
|
106
106
|
forwardToolCalls?: boolean;
|
|
107
107
|
/** Emit approval/action rich fragments instead of plain fallback text. */
|
|
108
108
|
richInteractions?: boolean;
|
|
109
|
+
/** Emit ONE consolidated awareness note to the agent when friend/conversation signals arrive. */
|
|
110
|
+
awarenessNote?: boolean;
|
|
109
111
|
reconnect?: OpenclawClawlingReconnectConfig;
|
|
110
112
|
heartbeat?: OpenclawClawlingHeartbeatConfig;
|
|
111
113
|
ack?: OpenclawClawlingAckConfig;
|
|
@@ -152,6 +154,7 @@ export const openclawClawlingConfigSchema = {
|
|
|
152
154
|
forwardThinking: { type: "boolean" },
|
|
153
155
|
forwardToolCalls: { type: "boolean" },
|
|
154
156
|
richInteractions: { type: "boolean" },
|
|
157
|
+
awarenessNote: { type: "boolean" },
|
|
155
158
|
reconnect: {
|
|
156
159
|
type: "object",
|
|
157
160
|
additionalProperties: false,
|
|
@@ -274,6 +277,7 @@ export type ResolvedOpenclawClawlingAccount = {
|
|
|
274
277
|
forwardThinking: boolean;
|
|
275
278
|
forwardToolCalls: boolean;
|
|
276
279
|
richInteractions: boolean;
|
|
280
|
+
awarenessNote: boolean;
|
|
277
281
|
allowFrom: string[];
|
|
278
282
|
reconnect: Required<OpenclawClawlingReconnectConfig>;
|
|
279
283
|
heartbeat: Required<OpenclawClawlingHeartbeatConfig>;
|
|
@@ -482,6 +486,8 @@ export function resolveOpenclawClawlingAccount(
|
|
|
482
486
|
typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
483
487
|
const richInteractions =
|
|
484
488
|
typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
489
|
+
const awarenessNote =
|
|
490
|
+
typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
|
|
485
491
|
|
|
486
492
|
return {
|
|
487
493
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
@@ -508,6 +514,7 @@ export function resolveOpenclawClawlingAccount(
|
|
|
508
514
|
forwardThinking,
|
|
509
515
|
forwardToolCalls,
|
|
510
516
|
richInteractions,
|
|
517
|
+
awarenessNote,
|
|
511
518
|
allowFrom: [],
|
|
512
519
|
reconnect: readReconnect(channel.reconnect),
|
|
513
520
|
heartbeat: readHeartbeat(channel.heartbeat),
|