@clawling/clawchat-plugin-openclaw 2026.7.6-3 → 2026.7.8-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/config.js +3 -0
- package/dist/src/liveware-sample.js +538 -0
- package/dist/src/runtime.js +70 -3
- package/dist/src/skill-update.js +66 -5
- package/dist/src/storage.js +65 -0
- package/openclaw.plugin.json +2 -0
- package/package.json +2 -2
- package/skills/clawchat-liveware-sample/SKILL.md +54 -0
- package/skills/clawchat-set-greeting/SKILL.md +44 -0
- package/skills/manifest.json +69 -0
- package/src/config.ts +7 -0
- package/src/liveware-sample.ts +625 -0
- package/src/runtime.ts +70 -2
- package/src/skill-update.ts +68 -6
- package/src/storage.ts +121 -0
package/src/runtime.ts
CHANGED
|
@@ -17,7 +17,9 @@ import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from ".
|
|
|
17
17
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
18
|
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
19
19
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
20
|
-
import
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { ensureLivewareCli, resolveLivewarePath } from "./liveware-cli.ts";
|
|
22
|
+
import { LivewareSampleSupervisor, type LivewareSampleStoreLike } from "./liveware-sample.ts";
|
|
21
23
|
import { ClawlingApiError } from "./api-types.ts";
|
|
22
24
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
23
25
|
import {
|
|
@@ -38,6 +40,7 @@ import type { ClawlingChatClient } from "./ws-client.ts";
|
|
|
38
40
|
import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
|
|
39
41
|
import {
|
|
40
42
|
PendingConsentStore,
|
|
43
|
+
cleanupTombstonedManagedSkills,
|
|
41
44
|
handleOwnerConsentReply,
|
|
42
45
|
managedSkillExists,
|
|
43
46
|
readLocalSkillState,
|
|
@@ -978,7 +981,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
978
981
|
log,
|
|
979
982
|
});
|
|
980
983
|
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
981
|
-
|
|
984
|
+
// Keep the promise: the liveware-sample supervisor must await it, or a
|
|
985
|
+
// first boot races the download, sees no CLI, and silently skips forever.
|
|
986
|
+
const livewareCliReady = ensureLivewareCli({
|
|
982
987
|
stateDir: runtime.state?.resolveStateDir?.(),
|
|
983
988
|
log: {
|
|
984
989
|
warn: (m) => log?.info?.(m),
|
|
@@ -1096,6 +1101,46 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1096
1101
|
conversationApiClient = undefined;
|
|
1097
1102
|
};
|
|
1098
1103
|
|
|
1104
|
+
// Liveware Sample auto-boot (spec: docs/superpowers/specs/2026-07-06-liveware-sample-autoboot-design.md).
|
|
1105
|
+
// Fire-and-forget like ensureLivewareCli above: never blocks or fails the channel.
|
|
1106
|
+
// `livewareSampleNotify` is reassigned once `client`/`account` are ready (see
|
|
1107
|
+
// `sendOwnerSkillMessage` below) — both live in this same function closure, so
|
|
1108
|
+
// the forward reference here just resolves to the fresh value at call time.
|
|
1109
|
+
let livewareSampleNotify: (text: string) => Promise<boolean> = async () => false;
|
|
1110
|
+
if (store) {
|
|
1111
|
+
const stateDirForSample = runtime.state?.resolveStateDir?.();
|
|
1112
|
+
const livewareSampleSupervisor = new LivewareSampleSupervisor({
|
|
1113
|
+
platform: "openclaw",
|
|
1114
|
+
accountId,
|
|
1115
|
+
enabled: account.livewareSample,
|
|
1116
|
+
// `store` here is the `ClawChatStore` returned by `resolveConnectionStore`;
|
|
1117
|
+
// `RuntimeConnectionStore` only `Pick`s a subset of its methods, so widen
|
|
1118
|
+
// through the structural `LivewareSampleStoreLike` contract (Task 8/4).
|
|
1119
|
+
store: store as unknown as LivewareSampleStoreLike,
|
|
1120
|
+
sampleRoot: path.join(stateDirForSample ?? "", "clawchat", "liveware-sample"),
|
|
1121
|
+
resolveToken: () => account.token ?? "",
|
|
1122
|
+
resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
|
|
1123
|
+
cliReady: livewareCliReady,
|
|
1124
|
+
listApps: async () => {
|
|
1125
|
+
const client = getConversationApiClient();
|
|
1126
|
+
return await client.listApps();
|
|
1127
|
+
},
|
|
1128
|
+
registerApp: async (p) => {
|
|
1129
|
+
const client = getConversationApiClient();
|
|
1130
|
+
return await client.registerApp(p);
|
|
1131
|
+
},
|
|
1132
|
+
notifyOwner: (text) => livewareSampleNotify(text),
|
|
1133
|
+
fetchFn: globalThis.fetch,
|
|
1134
|
+
log: {
|
|
1135
|
+
warn: (m) => log?.info?.(m),
|
|
1136
|
+
debug: (m) => log?.debug?.(m),
|
|
1137
|
+
error: (m) => log?.error?.(m),
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
abortSignal.addEventListener("abort", () => livewareSampleSupervisor.stop());
|
|
1141
|
+
void livewareSampleSupervisor.start();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1099
1144
|
const resolveMutateConfigFile = (): OpenclawClawchatMutateConfigFile | undefined => {
|
|
1100
1145
|
if (params.mutateConfigFile) return params.mutateConfigFile;
|
|
1101
1146
|
const runtimeConfig = runtime.config as unknown as {
|
|
@@ -2235,6 +2280,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2235
2280
|
}`,
|
|
2236
2281
|
);
|
|
2237
2282
|
}
|
|
2283
|
+
// Load-time tombstone sweep: delete any managed skill copy the bundled
|
|
2284
|
+
// manifest snapshot marks as removed, so a stale id from an old install
|
|
2285
|
+
// stops shadowing/loading. Fire-and-forget — must never block or fail boot.
|
|
2286
|
+
void cleanupTombstonedManagedSkills({
|
|
2287
|
+
managedDir: managedSkillsDir,
|
|
2288
|
+
bundledDir: bundledSkillsDir,
|
|
2289
|
+
log,
|
|
2290
|
+
}).catch(() => {});
|
|
2238
2291
|
// A usr_... id is not a valid chat_id — member-backend rejects it with
|
|
2239
2292
|
// "invalid conversation id" and msghub never acks the send. Proactive owner
|
|
2240
2293
|
// messages must be addressed to the direct conversation recorded at
|
|
@@ -2263,6 +2316,21 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2263
2316
|
log,
|
|
2264
2317
|
});
|
|
2265
2318
|
};
|
|
2319
|
+
// Wire the liveware-sample intro notifier now that `client`/`account` are in
|
|
2320
|
+
// scope (both declared earlier in this same function body — see the
|
|
2321
|
+
// `livewareSampleNotify` forward declaration above the supervisor construction).
|
|
2322
|
+
livewareSampleNotify = async (text: string): Promise<boolean> => {
|
|
2323
|
+
const conversationId = resolveOwnerDirectConversationId();
|
|
2324
|
+
if (!conversationId) return false;
|
|
2325
|
+
const result = await sendOpenclawClawlingText({
|
|
2326
|
+
client,
|
|
2327
|
+
account,
|
|
2328
|
+
to: { chatId: conversationId, chatType: "direct" },
|
|
2329
|
+
text,
|
|
2330
|
+
log,
|
|
2331
|
+
});
|
|
2332
|
+
return result != null;
|
|
2333
|
+
};
|
|
2266
2334
|
const triggerSkillUpdateCheck = async (): Promise<void> => {
|
|
2267
2335
|
const ownerUserId = account.ownerUserId?.trim();
|
|
2268
2336
|
if (!ownerUserId) {
|
package/src/skill-update.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -69,9 +69,27 @@ export const MAX_SKILL_BYTES = 256 * 1024;
|
|
|
69
69
|
/** This adapter's host target inside `skills/manifest.json`. */
|
|
70
70
|
export const SKILL_TARGET = "openclaw";
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Skill ids bundled with this plugin, discovered by scanning the bundled
|
|
74
|
+
* skills dir. Returns [] when the dir is unresolvable (managed-only mode —
|
|
75
|
+
* nothing bundled to protect from tombstones). Never hard-code this list.
|
|
76
|
+
*/
|
|
77
|
+
export function getBundledSkillIds(fromUrl?: string): readonly string[] {
|
|
78
|
+
let dir: string;
|
|
79
|
+
try {
|
|
80
|
+
dir = fromUrl === undefined ? resolveBundledSkillsDir() : resolveBundledSkillsDir(fromUrl);
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
86
|
+
.filter((e) => e.isDirectory() && existsSync(path.join(dir, e.name, "SKILL.md")))
|
|
87
|
+
.map((e) => e.name)
|
|
88
|
+
.sort();
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
75
93
|
|
|
76
94
|
// ---------------------------------------------------------------------------
|
|
77
95
|
// Minimal fetch abstraction (compatible with the global `fetch`).
|
|
@@ -490,6 +508,50 @@ export async function atomicWriteSkill(
|
|
|
490
508
|
}
|
|
491
509
|
}
|
|
492
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Load-time counterpart of the dynamic removal path: delete managed copies of
|
|
513
|
+
* ids tombstoned by the bundled manifest snapshot, so stale ids from an old
|
|
514
|
+
* install stop shadowing/loading. Never throws — a bad snapshot must not
|
|
515
|
+
* break plugin startup. Returns the removed ids.
|
|
516
|
+
*/
|
|
517
|
+
export async function cleanupTombstonedManagedSkills(options: {
|
|
518
|
+
managedDir: string;
|
|
519
|
+
bundledDir: string | null;
|
|
520
|
+
log?: { info?: (msg: string) => void };
|
|
521
|
+
}): Promise<string[]> {
|
|
522
|
+
if (!options.bundledDir) return [];
|
|
523
|
+
let removedIds: string[];
|
|
524
|
+
try {
|
|
525
|
+
const raw = await fs.readFile(path.join(options.bundledDir, "manifest.json"), "utf8");
|
|
526
|
+
const manifest = parseSkillsManifest(raw);
|
|
527
|
+
removedIds = manifest.removed[SKILL_TARGET] ?? [];
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const bundledIds = getBundledSkillIds();
|
|
532
|
+
const removed: string[] = [];
|
|
533
|
+
for (const id of removedIds) {
|
|
534
|
+
if (bundledIds.includes(id)) continue;
|
|
535
|
+
if (!existsSync(path.join(options.managedDir, id, "SKILL.md"))) continue;
|
|
536
|
+
// Per-id guard: one id's fs failure (EBUSY, EPERM, a directory squatting
|
|
537
|
+
// on the SKILL.md path, ...) must not reject the whole sweep nor discard
|
|
538
|
+
// the ids already removed — log it and move on to the next id.
|
|
539
|
+
try {
|
|
540
|
+
await removeManagedSkill(options.managedDir, id);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
options.log?.info?.(
|
|
543
|
+
`clawchat skill-update: failed to remove tombstoned managed skill ${id} at load: ${
|
|
544
|
+
err instanceof Error ? err.message : String(err)
|
|
545
|
+
}`,
|
|
546
|
+
);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
removed.push(id);
|
|
550
|
+
options.log?.info?.(`clawchat skill-update: removed tombstoned managed skill ${id} at load`);
|
|
551
|
+
}
|
|
552
|
+
return removed;
|
|
553
|
+
}
|
|
554
|
+
|
|
493
555
|
/**
|
|
494
556
|
* Delete a tombstoned managed skill: remove `<skillsDir>/<skillId>/SKILL.md`
|
|
495
557
|
* (the host chokidar-watches the dir; the unlink triggers a snapshot rebuild
|
|
@@ -684,7 +746,7 @@ export async function runSkillUpdateCheck(
|
|
|
684
746
|
|
|
685
747
|
const removals: PendingSkillRemoval[] = [];
|
|
686
748
|
for (const skillId of outcome.removedIds) {
|
|
687
|
-
if ((
|
|
749
|
+
if (getBundledSkillIds().includes(skillId)) {
|
|
688
750
|
options.log?.info?.(`clawchat skill-update: ignoring tombstone for bundled skill ${skillId}`);
|
|
689
751
|
continue;
|
|
690
752
|
}
|
|
@@ -810,7 +872,7 @@ export async function handleOwnerConsentReply(
|
|
|
810
872
|
// Defensive re-guard: bundled skills must never be deleted, even though
|
|
811
873
|
// runSkillUpdateCheck already filters them out of removal candidates.
|
|
812
874
|
// Unreachable in the normal flow; kept as a cheap safety net.
|
|
813
|
-
if ((
|
|
875
|
+
if (getBundledSkillIds().includes(removal.skillId)) {
|
|
814
876
|
options.log?.info?.(
|
|
815
877
|
`clawchat skill-update: skipped removal of bundled skill ${removal.skillId} (defensive re-guard)`,
|
|
816
878
|
);
|
package/src/storage.ts
CHANGED
|
@@ -157,6 +157,40 @@ export type ActivationConversation = {
|
|
|
157
157
|
lastRefreshedAt: number | null;
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
export type LivewareSampleRow = {
|
|
161
|
+
platform: string;
|
|
162
|
+
account_id: string;
|
|
163
|
+
app_id: string;
|
|
164
|
+
app_name: string;
|
|
165
|
+
port: number;
|
|
166
|
+
public_url: string | null;
|
|
167
|
+
sample_version: string;
|
|
168
|
+
status: string;
|
|
169
|
+
last_error: string | null;
|
|
170
|
+
intro_sent: number;
|
|
171
|
+
created_at: number;
|
|
172
|
+
updated_at: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type LivewareSampleUpsert = {
|
|
176
|
+
platform: string;
|
|
177
|
+
accountId: string;
|
|
178
|
+
appId: string;
|
|
179
|
+
appName: string;
|
|
180
|
+
port: number;
|
|
181
|
+
publicUrl?: string | null;
|
|
182
|
+
sampleVersion: string;
|
|
183
|
+
status: "active" | "failed" | "disabled";
|
|
184
|
+
lastError?: string | null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type UpdateLivewareSampleStatusInput = {
|
|
188
|
+
platform: string;
|
|
189
|
+
accountId: string;
|
|
190
|
+
status: "active" | "failed" | "disabled";
|
|
191
|
+
lastError?: string | null;
|
|
192
|
+
};
|
|
193
|
+
|
|
160
194
|
type Migration = { version: number; name: string; sql: string };
|
|
161
195
|
|
|
162
196
|
const DB_FILENAME = "clawchat.sqlite";
|
|
@@ -290,6 +324,27 @@ ALTER TABLE clawchat_messages ADD COLUMN send_error TEXT;
|
|
|
290
324
|
name: "activation_device_id",
|
|
291
325
|
sql: `
|
|
292
326
|
ALTER TABLE activations ADD COLUMN device_id TEXT;
|
|
327
|
+
`,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
version: 8,
|
|
331
|
+
name: "liveware_sample",
|
|
332
|
+
sql: `
|
|
333
|
+
CREATE TABLE IF NOT EXISTS liveware_sample (
|
|
334
|
+
platform TEXT NOT NULL,
|
|
335
|
+
account_id TEXT NOT NULL,
|
|
336
|
+
app_id TEXT NOT NULL,
|
|
337
|
+
app_name TEXT NOT NULL,
|
|
338
|
+
port INTEGER NOT NULL,
|
|
339
|
+
public_url TEXT,
|
|
340
|
+
sample_version TEXT NOT NULL,
|
|
341
|
+
status TEXT NOT NULL,
|
|
342
|
+
last_error TEXT,
|
|
343
|
+
intro_sent INTEGER NOT NULL DEFAULT 0,
|
|
344
|
+
created_at INTEGER NOT NULL,
|
|
345
|
+
updated_at INTEGER NOT NULL,
|
|
346
|
+
PRIMARY KEY (platform, account_id)
|
|
347
|
+
);
|
|
293
348
|
`,
|
|
294
349
|
},
|
|
295
350
|
];
|
|
@@ -810,6 +865,72 @@ export class ClawChatStore {
|
|
|
810
865
|
}) ?? null;
|
|
811
866
|
}
|
|
812
867
|
|
|
868
|
+
getLivewareSample(input: ConversationAccountInput): LivewareSampleRow | null {
|
|
869
|
+
return this.read(() => {
|
|
870
|
+
const row = this.requireDb()
|
|
871
|
+
.prepare(`SELECT * FROM liveware_sample WHERE platform = ? AND account_id = ?`)
|
|
872
|
+
.get(input.platform, input.accountId);
|
|
873
|
+
return (row as LivewareSampleRow | undefined) ?? null;
|
|
874
|
+
}) ?? null;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
upsertLivewareSample(input: LivewareSampleUpsert): void {
|
|
878
|
+
this.write(() => {
|
|
879
|
+
const now = Date.now();
|
|
880
|
+
this.requireDb()
|
|
881
|
+
.prepare(
|
|
882
|
+
`INSERT INTO liveware_sample
|
|
883
|
+
(platform, account_id, app_id, app_name, port, public_url,
|
|
884
|
+
sample_version, status, last_error, intro_sent, created_at, updated_at)
|
|
885
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
886
|
+
ON CONFLICT(platform, account_id) DO UPDATE SET
|
|
887
|
+
app_id = excluded.app_id,
|
|
888
|
+
app_name = excluded.app_name,
|
|
889
|
+
port = excluded.port,
|
|
890
|
+
public_url = excluded.public_url,
|
|
891
|
+
sample_version = excluded.sample_version,
|
|
892
|
+
status = excluded.status,
|
|
893
|
+
last_error = excluded.last_error,
|
|
894
|
+
updated_at = excluded.updated_at`,
|
|
895
|
+
)
|
|
896
|
+
.run(
|
|
897
|
+
input.platform,
|
|
898
|
+
input.accountId,
|
|
899
|
+
input.appId,
|
|
900
|
+
input.appName,
|
|
901
|
+
input.port,
|
|
902
|
+
input.publicUrl ?? null,
|
|
903
|
+
input.sampleVersion,
|
|
904
|
+
input.status,
|
|
905
|
+
input.lastError ?? null,
|
|
906
|
+
now,
|
|
907
|
+
now,
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
updateLivewareSampleStatus(input: UpdateLivewareSampleStatusInput): void {
|
|
913
|
+
this.write(() => {
|
|
914
|
+
this.requireDb()
|
|
915
|
+
.prepare(
|
|
916
|
+
`UPDATE liveware_sample SET status = ?, last_error = ?, updated_at = ?
|
|
917
|
+
WHERE platform = ? AND account_id = ?`,
|
|
918
|
+
)
|
|
919
|
+
.run(input.status, input.lastError ?? null, Date.now(), input.platform, input.accountId);
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
markLivewareSampleIntroSent(input: ConversationAccountInput): void {
|
|
924
|
+
this.write(() => {
|
|
925
|
+
this.requireDb()
|
|
926
|
+
.prepare(
|
|
927
|
+
`UPDATE liveware_sample SET intro_sent = 1, updated_at = ?
|
|
928
|
+
WHERE platform = ? AND account_id = ?`,
|
|
929
|
+
)
|
|
930
|
+
.run(Date.now(), input.platform, input.accountId);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
813
934
|
recordToolCall(input: ToolCallInput): void {
|
|
814
935
|
this.write(() => {
|
|
815
936
|
const startedAt = input.startedAt ?? Date.now();
|