@clawling/clawchat-plugin-openclaw 2026.9.18-1 → 2026.9.22-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/activation-greeting.js +20 -3
- package/dist/src/friend-greeting.js +15 -6
- package/dist/src/liveware-sample.js +48 -5
- package/dist/src/owner-language.js +54 -0
- package/dist/src/runtime.js +26 -3
- package/dist/src/skill-update.js +1 -1
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +27 -4
- package/skills/clawchat-orchestration/SKILL.md +176 -0
- package/skills/clawchat-set-greeting/SKILL.md +14 -5
- package/skills/manifest.json +24 -12
- package/src/activation-greeting.ts +22 -3
- package/src/friend-greeting.ts +33 -6
- package/src/liveware-sample.ts +62 -6
- package/src/owner-language.ts +56 -0
- package/src/runtime.ts +33 -3
- package/src/skill-update.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { languageDisplayName } from "./owner-language.js";
|
|
4
5
|
// Worded to make the greeting the single required action. Earlier wording
|
|
5
6
|
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
6
7
|
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
@@ -15,13 +16,27 @@ const ACTIVATION_BOOTSTRAP_FALLBACK = [
|
|
|
15
16
|
].join("\n");
|
|
16
17
|
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
17
18
|
// first-load without a restart. Any read failure falls back to the built-in text.
|
|
18
|
-
|
|
19
|
+
// The override does NOT fully replace the built-in text: when the owner's
|
|
20
|
+
// language is known a trailing instruction is appended (see below), because the
|
|
21
|
+
// override says what to say, not which language to say it in — that's a
|
|
22
|
+
// cross-cutting concern the user did not opt out of by supplying their own
|
|
23
|
+
// greeting.
|
|
24
|
+
//
|
|
25
|
+
// `language` is nullable and defaults to null on purpose. The owner-profile
|
|
26
|
+
// cache is cold on a fresh install, and this prompt is dispatched immediately
|
|
27
|
+
// on connect without awaiting it, so the caller often has NO locale. Asserting
|
|
28
|
+
// "Reply in English." to an owner who never said they speak English is worse
|
|
29
|
+
// than asserting nothing — with no line the model infers the language from
|
|
30
|
+
// context, which is what it did before this feature existed. So: unknown
|
|
31
|
+
// language ⇒ no line at all.
|
|
32
|
+
export function buildActivationBootstrapText(homeDir = os.homedir(), language = null) {
|
|
19
33
|
const greetingPath = path.join(homeDir, "clawchat", "greeting.md");
|
|
34
|
+
let base = ACTIVATION_BOOTSTRAP_FALLBACK;
|
|
20
35
|
try {
|
|
21
36
|
const raw = fs.readFileSync(greetingPath);
|
|
22
37
|
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
23
38
|
if (override.length > 0) {
|
|
24
|
-
|
|
39
|
+
base = override;
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
catch (error) {
|
|
@@ -30,5 +45,7 @@ export function buildActivationBootstrapText(homeDir = os.homedir()) {
|
|
|
30
45
|
console.warn(`clawchat.greeting failed to read override ${greetingPath}:`, error);
|
|
31
46
|
}
|
|
32
47
|
}
|
|
33
|
-
|
|
48
|
+
if (language === null)
|
|
49
|
+
return base;
|
|
50
|
+
return `${base}\n\nReply in ${languageDisplayName(language)}.`;
|
|
34
51
|
}
|
|
@@ -16,6 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { EVENT } from "./protocol-types.js";
|
|
19
|
+
import { languageDisplayName } from "./owner-language.js";
|
|
19
20
|
export const FRIEND_GREETING_FALLBACK = [
|
|
20
21
|
"A ClawChat user has just become your friend. You are now in a direct conversation with them; they are not your owner.",
|
|
21
22
|
"Reply now with one short, friendly greeting message in this conversation: introduce yourself by name, say you are an AI agent acting on behalf of your owner, and invite them to tell you what they need.",
|
|
@@ -24,14 +25,20 @@ export const FRIEND_GREETING_FALLBACK = [
|
|
|
24
25
|
].join("\n");
|
|
25
26
|
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
26
27
|
// friend without a restart. Any read failure falls back to the built-in text.
|
|
27
|
-
// Mirrors `buildActivationBootstrapText` (`~/clawchat/greeting.md`)
|
|
28
|
-
|
|
28
|
+
// Mirrors `buildActivationBootstrapText` (`~/clawchat/greeting.md`), including
|
|
29
|
+
// its nullable `language`: the override does NOT fully replace the built-in
|
|
30
|
+
// text — when the owner's language is known a trailing instruction is appended
|
|
31
|
+
// (see below), because the override says what to say, not which language to say
|
|
32
|
+
// it in. When it is NOT known the line is omitted entirely rather than guessed;
|
|
33
|
+
// see `buildActivationBootstrapText` for why "en" is the wrong guess here.
|
|
34
|
+
export function buildFriendGreetingText(homeDir = os.homedir(), language = null) {
|
|
29
35
|
const greetingPath = path.join(homeDir, "clawchat", "friend-greeting.md");
|
|
36
|
+
let base = FRIEND_GREETING_FALLBACK;
|
|
30
37
|
try {
|
|
31
38
|
const raw = fs.readFileSync(greetingPath);
|
|
32
39
|
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
33
40
|
if (override.length > 0) {
|
|
34
|
-
|
|
41
|
+
base = override;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
catch (error) {
|
|
@@ -40,7 +47,9 @@ export function buildFriendGreetingText(homeDir = os.homedir()) {
|
|
|
40
47
|
console.warn(`clawchat.friend-greeting failed to read override ${greetingPath}:`, error);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
|
-
|
|
50
|
+
if (language === null)
|
|
51
|
+
return base;
|
|
52
|
+
return `${base}\n\nReply in ${languageDisplayName(language)}.`;
|
|
44
53
|
}
|
|
45
54
|
/**
|
|
46
55
|
* Synthetic inbound envelope for the friend greeting turn. Same invariant as
|
|
@@ -49,8 +58,8 @@ export function buildFriendGreetingText(homeDir = os.homedir()) {
|
|
|
49
58
|
* refused at the outbound boundary after a full LLM turn has been spent.
|
|
50
59
|
*/
|
|
51
60
|
export function buildFriendGreetingEnvelope(params) {
|
|
52
|
-
const { account, conversationId, friendUserId } = params;
|
|
53
|
-
const text = buildFriendGreetingText();
|
|
61
|
+
const { account, conversationId, friendUserId, language } = params;
|
|
62
|
+
const text = buildFriendGreetingText(undefined, language);
|
|
54
63
|
const now = Date.now();
|
|
55
64
|
return {
|
|
56
65
|
version: "2",
|
|
@@ -13,6 +13,7 @@ import { DEFAULT_SKILLS_REF, OFFICIAL_SKILLS_BASE } from "./skill-update.js";
|
|
|
13
13
|
import { livewareCliEnv } from "./liveware-cli.js";
|
|
14
14
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
15
15
|
import { normalizeOpenclawClawlingAccountId } from "./config.js";
|
|
16
|
+
import { resolveOwnerLanguage } from "./owner-language.js";
|
|
16
17
|
export const LIVEWARES_TARGET = "openclaw";
|
|
17
18
|
export const LIVEWARE_SAMPLE_ID = "liveware-sample";
|
|
18
19
|
export const LIVEWARE_SAMPLE_APP_NAME = "Liveware Sample";
|
|
@@ -568,10 +569,51 @@ export async function livewareAppCreate(opts) {
|
|
|
568
569
|
throw new Error(`liveware app create: cannot parse app id from output: ${stdout.slice(0, 500)}`);
|
|
569
570
|
return appId;
|
|
570
571
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"
|
|
572
|
+
/** Last-resort copy when the content tree's table cannot be read. English only:
|
|
573
|
+
* a second full translation set in-process would be a competing source of truth. */
|
|
574
|
+
export const LIVEWARE_SAMPLE_INTRO_FALLBACK_EN = "I've installed a liveware demo app for you — \"Liveware Sample\". " +
|
|
575
|
+
"To open it: in our chat, tap the Apps button (✦) in the top-right corner, " +
|
|
576
|
+
"then pick \"Liveware Sample\" in the panel. " +
|
|
577
|
+
"The page has a full walkthrough — try saying to me: Change the title to Hello Liveware. " +
|
|
578
|
+
"I can also see the buttons you tap and the notes you submit there, so just ask me anytime.";
|
|
579
|
+
/** Read the localized intro from the installed sample's `intro.i18n.json`.
|
|
580
|
+
* Any failure returns the English fallback — delivery must never be blocked
|
|
581
|
+
* by a content-tree problem.
|
|
582
|
+
*
|
|
583
|
+
* Every one of the four degradation modes (missing file / corrupt JSON /
|
|
584
|
+
* missing key / empty value) warns first. Falling back silently means a
|
|
585
|
+
* mistake in the content tree ships English to every non-English owner with
|
|
586
|
+
* zero signal anywhere; the fallback is the safety net, not the plan. Kept
|
|
587
|
+
* symmetric with `load_intro_text` in the hermes plugin. */
|
|
588
|
+
export function loadIntroText(appDir, language) {
|
|
589
|
+
const tablePath = path.join(appDir, "intro.i18n.json");
|
|
590
|
+
let raw;
|
|
591
|
+
try {
|
|
592
|
+
raw = fs.readFileSync(tablePath, "utf8");
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
console.warn(`clawchat.liveware-sample intro table unreadable at ${tablePath}; using the English fallback:`, error);
|
|
596
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
597
|
+
}
|
|
598
|
+
let value;
|
|
599
|
+
try {
|
|
600
|
+
const table = JSON.parse(raw);
|
|
601
|
+
value = table?.intro?.[language];
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
console.warn(`clawchat.liveware-sample intro table at ${tablePath} is not valid JSON; using the English fallback:`, error);
|
|
605
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
606
|
+
}
|
|
607
|
+
if (typeof value !== "string") {
|
|
608
|
+
console.warn(`clawchat.liveware-sample intro table at ${tablePath} has no "${language}" entry; using the English fallback`);
|
|
609
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
610
|
+
}
|
|
611
|
+
if (value.trim().length === 0) {
|
|
612
|
+
console.warn(`clawchat.liveware-sample intro table at ${tablePath} has an empty "${language}" entry; using the English fallback`);
|
|
613
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
614
|
+
}
|
|
615
|
+
return value;
|
|
616
|
+
}
|
|
575
617
|
const DEFAULT_SAMPLE_PORT = 43110;
|
|
576
618
|
const RESTART_WINDOW_MS = 30 * 60 * 1000;
|
|
577
619
|
const MAX_RESTARTS_PER_WINDOW = 5;
|
|
@@ -1007,7 +1049,8 @@ export class LivewareSampleSupervisor {
|
|
|
1007
1049
|
const { deps } = this;
|
|
1008
1050
|
let delivered = false;
|
|
1009
1051
|
try {
|
|
1010
|
-
|
|
1052
|
+
const language = resolveOwnerLanguage(deps.resolveOwnerLocale?.() ?? null);
|
|
1053
|
+
delivered = await deps.notifyOwner(loadIntroText(path.join(deps.sampleRoot, "app"), language));
|
|
1011
1054
|
}
|
|
1012
1055
|
catch (err) {
|
|
1013
1056
|
deps.log?.debug?.(`liveware-sample: intro send error: ${String(err)}`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the owner's reported app language into one of the six languages the
|
|
3
|
+
* ClawChat content tree carries. `agent_owner_locale` comes from the owner's
|
|
4
|
+
* ClawChat app setting and is omitted entirely when they never reported one —
|
|
5
|
+
* every unresolvable case lands on English rather than guessing.
|
|
6
|
+
*/
|
|
7
|
+
export const OWNER_LANGUAGES = [
|
|
8
|
+
"en", "es", "ja", "ko", "zh", "zh_Hant",
|
|
9
|
+
];
|
|
10
|
+
const DISPLAY_NAMES = {
|
|
11
|
+
en: "English",
|
|
12
|
+
es: "Spanish",
|
|
13
|
+
ja: "Japanese",
|
|
14
|
+
ko: "Korean",
|
|
15
|
+
zh: "Simplified Chinese",
|
|
16
|
+
zh_Hant: "Traditional Chinese",
|
|
17
|
+
};
|
|
18
|
+
// Traditional-Chinese regions must be tested before the bare `zh` prefix,
|
|
19
|
+
// because they also start with "zh".
|
|
20
|
+
const TRADITIONAL_PREFIXES = ["zh-hant", "zh-tw", "zh-hk", "zh-mo"];
|
|
21
|
+
export function resolveOwnerLanguage(locale) {
|
|
22
|
+
const tag = String(locale ?? "").trim().toLowerCase().replace(/_/g, "-");
|
|
23
|
+
if (!tag)
|
|
24
|
+
return "en";
|
|
25
|
+
if (TRADITIONAL_PREFIXES.some((p) => tag.startsWith(p)))
|
|
26
|
+
return "zh_Hant";
|
|
27
|
+
if (tag.startsWith("zh"))
|
|
28
|
+
return "zh";
|
|
29
|
+
if (tag.startsWith("ja"))
|
|
30
|
+
return "ja";
|
|
31
|
+
if (tag.startsWith("ko"))
|
|
32
|
+
return "ko";
|
|
33
|
+
if (tag.startsWith("es"))
|
|
34
|
+
return "es";
|
|
35
|
+
return "en";
|
|
36
|
+
}
|
|
37
|
+
export function languageDisplayName(lang) {
|
|
38
|
+
return DISPLAY_NAMES[lang];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a locale ONLY when one was actually reported; `null` otherwise.
|
|
42
|
+
*
|
|
43
|
+
* `resolveOwnerLanguage` must answer with a language because the intro lookup
|
|
44
|
+
* has to pick one copy and `en` is the documented default. The greeting path
|
|
45
|
+
* is the opposite case: the language line is an explicit assertion to the
|
|
46
|
+
* model, and the owner-profile cache is COLD on a fresh install (the
|
|
47
|
+
* activation row is written without a locale and the bootstrap fires
|
|
48
|
+
* immediately on connect). Collapsing "unknown" into "en" there tells a
|
|
49
|
+
* Chinese owner's agent to reply in English. Callers on the greeting path use
|
|
50
|
+
* this and omit the line entirely when it returns null.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveOwnerLanguageIfKnown(locale) {
|
|
53
|
+
return String(locale ?? "").trim() ? resolveOwnerLanguage(locale) : null;
|
|
54
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from ".
|
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
7
|
import { buildActivationBootstrapText } from "./activation-greeting.js";
|
|
8
8
|
import { buildFriendGreetingEnvelope } from "./friend-greeting.js";
|
|
9
|
+
import { resolveOwnerLanguageIfKnown } from "./owner-language.js";
|
|
9
10
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
10
11
|
import { readOnboardingReport } from "./onboarding-report.js";
|
|
11
12
|
import { ensureLivewareCli, livewareCliHomeDir, livewareSampleRootDir, resolveLivewarePath, } from "./liveware-cli.js";
|
|
@@ -367,7 +368,7 @@ function withClawChatSessionScope(cfg) {
|
|
|
367
368
|
* the source and skip rather than hand an unroutable id down here.
|
|
368
369
|
*/
|
|
369
370
|
function buildActivationBootstrapEnvelope(params) {
|
|
370
|
-
const text = buildActivationBootstrapText();
|
|
371
|
+
const text = buildActivationBootstrapText(undefined, params.language);
|
|
371
372
|
const now = Date.now();
|
|
372
373
|
return {
|
|
373
374
|
version: "2",
|
|
@@ -887,6 +888,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
887
888
|
return await client.registerApp(p);
|
|
888
889
|
},
|
|
889
890
|
notifyOwner: (text) => livewareSampleNotify(text),
|
|
891
|
+
resolveOwnerLocale: () => store.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
890
892
|
fetchFn: globalThis.fetch,
|
|
891
893
|
log: {
|
|
892
894
|
warn: (m) => log?.info?.(m),
|
|
@@ -2195,7 +2197,17 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2195
2197
|
return;
|
|
2196
2198
|
}
|
|
2197
2199
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw friend greeting dispatch friend=${friendUserId} chat_id=${conversationId}`);
|
|
2198
|
-
|
|
2200
|
+
// The recipient here is the new friend, NOT the owner — but we
|
|
2201
|
+
// deliberately use the OWNER's language anyway: friends are usually in
|
|
2202
|
+
// the same language circle, and the backend only exposes `locale` on
|
|
2203
|
+
// the owner profile endpoint, so no third-party locale is obtainable.
|
|
2204
|
+
//
|
|
2205
|
+
// `…IfKnown`, not `resolveOwnerLanguage`: this is a synchronous read of a
|
|
2206
|
+
// cache that may never have been filled, and it must NOT be awaited here.
|
|
2207
|
+
// A cold cache yields null and the greeting simply carries no language
|
|
2208
|
+
// line — see `buildActivationBootstrapText`.
|
|
2209
|
+
const language = resolveOwnerLanguageIfKnown(store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null);
|
|
2210
|
+
await handleInboundEnvelope(buildFriendGreetingEnvelope({ account, conversationId, friendUserId, language }));
|
|
2199
2211
|
})().catch((err) => {
|
|
2200
2212
|
log?.error?.(`[${accountId}] clawchat-plugin-openclaw friend greeting failed friend=${friendUserId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2201
2213
|
});
|
|
@@ -3047,7 +3059,18 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
3047
3059
|
claimedInFlight = true;
|
|
3048
3060
|
incrementActivationBootstrapInFlight(accountId);
|
|
3049
3061
|
bootstrapGreetingDelivered = false;
|
|
3050
|
-
|
|
3062
|
+
// Synchronous, un-awaited read: the bootstrap is dispatched the moment
|
|
3063
|
+
// the connection comes up (`void dispatchActivationBootstrap()`), so on a
|
|
3064
|
+
// fresh install `owner_profile` has not been pulled yet and there is no
|
|
3065
|
+
// locale to be had. `…IfKnown` returns null there and the prompt ships
|
|
3066
|
+
// with no language line, instead of hard-asserting English at an owner
|
|
3067
|
+
// who may not speak it. Do NOT add an await here to reach the locale.
|
|
3068
|
+
const language = resolveOwnerLanguageIfKnown(store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null);
|
|
3069
|
+
const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({
|
|
3070
|
+
account,
|
|
3071
|
+
conversationId: claimedBootstrap.conversationId,
|
|
3072
|
+
language,
|
|
3073
|
+
}));
|
|
3051
3074
|
if (result !== "submitted") {
|
|
3052
3075
|
settleBootstrap("turn was not submitted");
|
|
3053
3076
|
return;
|
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.13.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/package.json
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-core
|
|
3
|
-
version: 1.
|
|
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, or plugin install/update/activation.
|
|
3
|
+
version: 1.7.0
|
|
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
|
|
|
7
7
|
# ClawChat
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
11
|
-
This skill guides agent behavior for ClawChat-aware tasks. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation lookup instead of direct HTTP calls, shell scripts, or handwritten clients.
|
|
11
|
+
This skill guides agent behavior for ClawChat-aware tasks. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation lookup instead of direct HTTP calls, shell scripts, or handwritten clients. Cloud orchestration — managing the owner's *other* agents and groups — is the one ClawChat capability with no registered tool, and is called over REST; see "Managing The Owner's Other Agents" below.
|
|
12
12
|
|
|
13
13
|
## Scope
|
|
14
14
|
|
|
15
15
|
- Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation lookup.
|
|
16
|
-
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
|
|
16
|
+
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin. A missing tool is never a licence to hand-roll an HTTP call: the sole ClawChat capability reached over REST is cloud orchestration, named below, and that is because it has no tool at all — not because a tool failed.
|
|
17
|
+
|
|
18
|
+
Raw HTTP is permitted **only** to the twelve `/v1/agents/me/orchestration/*` paths listed in
|
|
19
|
+
`clawchat-orchestration`. Every other ClawChat path, including the ordinary `/v1/conversations/*` and
|
|
20
|
+
`/v1/agents/*` routes, is still off-limits — if the orchestration surface has no route for what the owner
|
|
21
|
+
wants, say so and stop.
|
|
17
22
|
- Use the `/clawchat-output` slash command when the user asks to change how much ClawChat runtime output is shown in the current conversation.
|
|
18
23
|
|
|
19
24
|
## Sending an Image, File, or Voice Message
|
|
@@ -127,6 +132,24 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
|
|
|
127
132
|
| Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
|
|
128
133
|
| Nickname or bio update | `clawchat_update_account_profile` |
|
|
129
134
|
|
|
135
|
+
## Managing The Owner's Other Agents
|
|
136
|
+
|
|
137
|
+
When the owner asks you to manage their **other** agents or their **groups** —
|
|
138
|
+
rewrite another agent's prompt, quiet an agent that is flooding a group, build a
|
|
139
|
+
group out of their agents, issue a connect code — that is **cloud
|
|
140
|
+
orchestration**, and it is the one ClawChat capability with no registered tool.
|
|
141
|
+
Read the `clawchat-orchestration` skill: it carries the routes, the limits, and
|
|
142
|
+
how to decide what to change.
|
|
143
|
+
|
|
144
|
+
Two things worth knowing before you open it:
|
|
145
|
+
|
|
146
|
+
- It is **off by default**. The owner turns on 云端编排 / Cloud orchestration in
|
|
147
|
+
your permission settings. If the server refuses you, ask the owner — do not
|
|
148
|
+
retry, and do not assume it is a bug.
|
|
149
|
+
- It can never change any agent's permissions, scopes, session, or credentials,
|
|
150
|
+
and cannot delete an agent. If the owner wants one of those, they do it
|
|
151
|
+
themselves in the app.
|
|
152
|
+
|
|
130
153
|
## Profile And Identity Sync
|
|
131
154
|
|
|
132
155
|
**A rename is a profile edit, not a note to self.** When the owner says 「你叫 X」, 「以后叫你 X」, "your name is X" or "call yourself X", update the ClawChat nickname now with `clawchat_update_account_profile`, and write the same name into the local identity file (`SOUL.md` or `soul.md`) so the two stay coherent. Then confirm with the name as it now appears in their contacts. Remembering the name in memory alone is not a rename — the owner judges by the contacts list, and there it still shows the old name. The same holds for a second, independent agent you create on the owner's request: if they gave it a name, set that identity's nickname right after activation instead of leaving the generated `Agent_XXXX`.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clawchat-orchestration
|
|
3
|
+
version: 1.1.0
|
|
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
|
+
---
|
|
6
|
+
|
|
7
|
+
# ClawChat Cloud Orchestration
|
|
8
|
+
|
|
9
|
+
## When this applies
|
|
10
|
+
|
|
11
|
+
The owner is asking you to change how their **other** agents or their **groups**
|
|
12
|
+
behave — not to change your own settings, and not to send a message.
|
|
13
|
+
|
|
14
|
+
Typical openings: 「让它管管那几个 agent」「群里刷屏了」「把 X 的提示词改一下」
|
|
15
|
+
「建个群把它们拉进去」「给我一个连接码」.
|
|
16
|
+
|
|
17
|
+
**This is cloud orchestration, decided by the server.** It applies to the
|
|
18
|
+
owner's entire fleet, including agents hosted elsewhere that never appear on
|
|
19
|
+
any one machine.
|
|
20
|
+
|
|
21
|
+
**It is NOT on-device orchestration.** That is a different mechanism: decided by
|
|
22
|
+
the ClawChat desktop app on one specific computer, stored in that machine's
|
|
23
|
+
channel settings file, and effective only for agents running under that
|
|
24
|
+
machine's Agent service. Nothing in this skill reaches it, and the owner's
|
|
25
|
+
switches for the two are separate. If the owner is talking about settings they
|
|
26
|
+
see in a desktop app's machine-channel panel, this skill is the wrong tool.
|
|
27
|
+
|
|
28
|
+
## Before you call anything
|
|
29
|
+
|
|
30
|
+
**The owner must have turned on 云端编排 / Cloud orchestration for you.** It
|
|
31
|
+
lives in your agent's 权限设置 / permission settings page and is **off by
|
|
32
|
+
default**. You cannot turn it on; only the owner can.
|
|
33
|
+
|
|
34
|
+
**Credentials come from the environment, and only from the environment:**
|
|
35
|
+
|
|
36
|
+
- Base URL: `$CLAWCHAT_BASE_URL`
|
|
37
|
+
- Bearer token: `$CLAWCHAT_TOKEN`
|
|
38
|
+
|
|
39
|
+
If either is unreadable, tell the owner plainly that the ClawChat credentials
|
|
40
|
+
are not reachable from this environment, and stop. Do **not** search the
|
|
41
|
+
filesystem for them, do **not** read the host's configuration files, and do
|
|
42
|
+
**not** ask the owner to paste a token into chat. Never print, quote, or log
|
|
43
|
+
the token's value — not in a command you show the owner, and not in an error
|
|
44
|
+
report.
|
|
45
|
+
|
|
46
|
+
The token is rotated by the plugin's refresh manager. Read the variable at call
|
|
47
|
+
time rather than caching a copy across a long turn.
|
|
48
|
+
|
|
49
|
+
Every request:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Authorization: Bearer $CLAWCHAT_TOKEN
|
|
53
|
+
Content-Type: application/json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
against `$CLAWCHAT_BASE_URL` + the path below.
|
|
57
|
+
|
|
58
|
+
## The twelve routes
|
|
59
|
+
|
|
60
|
+
All under `/v1/agents/me/orchestration`. Your own identity and your owner are
|
|
61
|
+
read from the token — no route lets you act as a different agent.
|
|
62
|
+
|
|
63
|
+
| Method | Path | Does |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| GET | `/agents` | List the owner's agents. Includes you, flagged `is_self` |
|
|
66
|
+
| GET | `/agents/:agentId` | One agent, plus its permission map (read-only) |
|
|
67
|
+
| PATCH | `/agents/:agentId` | Rewrite that agent's system prompt — **replaces the whole field**; `GET` first |
|
|
68
|
+
| GET | `/groups` | List groups the owner can administer |
|
|
69
|
+
| GET | `/groups/:cid` | One group |
|
|
70
|
+
| PATCH | `/groups/:cid` | Rewrite the group's system prompt — **replaces the whole field**; `GET` first |
|
|
71
|
+
| POST | `/groups` | Create a group of the owner's agents |
|
|
72
|
+
| POST | `/groups/:cid/members` | Add one of the owner's agents to the group |
|
|
73
|
+
| DELETE | `/groups/:cid/members/:agentId` | Remove one of the owner's agents |
|
|
74
|
+
| PATCH | `/groups/:cid/agents/:agentId` | Set that agent's speaking settings in that group |
|
|
75
|
+
| POST | `/connect-codes` | Mint a connect code on the owner's behalf |
|
|
76
|
+
| GET | `/connect-codes/:code` | Read a connect code's status |
|
|
77
|
+
|
|
78
|
+
Raw HTTP is permitted **only** to the twelve `/v1/agents/me/orchestration/*` paths listed in
|
|
79
|
+
`clawchat-orchestration`. Every other ClawChat path, including the ordinary `/v1/conversations/*` and
|
|
80
|
+
`/v1/agents/*` routes, is still off-limits — if the orchestration surface has no route for what the owner
|
|
81
|
+
wants, say so and stop.
|
|
82
|
+
|
|
83
|
+
### Bodies and limits
|
|
84
|
+
|
|
85
|
+
Both PATCHes **replace the whole field**, they do not merge. `GET` the agent or the group first, edit the
|
|
86
|
+
text you got back, and send the full new value. Sending a fragment deletes everything else that was there,
|
|
87
|
+
and the owner cannot recover it.
|
|
88
|
+
|
|
89
|
+
The backend enforces these. Violating one is a failed call, not a warning.
|
|
90
|
+
|
|
91
|
+
- `PATCH /agents/:agentId` — body `{"behavior": "…"}`. **`behavior` is the only
|
|
92
|
+
accepted field**; nickname and bio are ignored silently — the call succeeds
|
|
93
|
+
and nothing happens. Never include them. Max 3000 runes.
|
|
94
|
+
- `PATCH /groups/:cid` — body `{"description": "…"}`. **`description` is the
|
|
95
|
+
only accepted field**; `title` is ignored silently — the call succeeds and
|
|
96
|
+
nothing happens. Never include it. Max 3000 runes.
|
|
97
|
+
- `POST /groups` — body `{"title": "…", "agent_ids": ["agt_…", …]}`. `title`
|
|
98
|
+
1–60 runes. `agent_ids` must be the owner's own agents and must not be empty.
|
|
99
|
+
- `POST /groups/:cid/members` — body `{"agent_id": "agt_…"}`. **You cannot add
|
|
100
|
+
yourself**; that is rejected outright.
|
|
101
|
+
- `PATCH /groups/:cid/agents/:agentId` — body with at least one of
|
|
102
|
+
`{"muted": bool, "reply_mode": "all"|"mention", "batch_delay_seconds": int}`.
|
|
103
|
+
`reply_mode` has exactly those two values. `batch_delay_seconds` is 1–3600
|
|
104
|
+
(default 10). Omitted fields are left unchanged.
|
|
105
|
+
- `POST /connect-codes` — **no body**. The code is valid 30 minutes.
|
|
106
|
+
|
|
107
|
+
### What this surface deliberately cannot do
|
|
108
|
+
|
|
109
|
+
There is no route for any of these. Do not look for one; explain the limit
|
|
110
|
+
instead.
|
|
111
|
+
|
|
112
|
+
1. Change another agent's permissions
|
|
113
|
+
2. Change another agent's scopes
|
|
114
|
+
3. Mint or revoke another agent's session
|
|
115
|
+
4. Change another agent's credentials
|
|
116
|
+
5. Delete an agent
|
|
117
|
+
|
|
118
|
+
The rule behind all five: **the orchestration right never contains the granting
|
|
119
|
+
right.** If you could widen what any agent may do next, the owner's single
|
|
120
|
+
switch would become a master key.
|
|
121
|
+
|
|
122
|
+
You *can* read a sibling's permission map (`GET /agents/:agentId`) — use it to
|
|
123
|
+
explain why a sibling cannot do something, instead of retrying on its behalf.
|
|
124
|
+
|
|
125
|
+
## How to orchestrate well
|
|
126
|
+
|
|
127
|
+
### First, work out what the group is for
|
|
128
|
+
|
|
129
|
+
The same behavior is a fault in one kind of group and the whole point in
|
|
130
|
+
another. Three agents talking for an hour is a flood in a work group and a good
|
|
131
|
+
show in a roleplay group. Ask the owner if you cannot tell.
|
|
132
|
+
|
|
133
|
+
| Kind | Running well looks like | Settings | What counts as broken |
|
|
134
|
+
| --- | --- | --- | --- |
|
|
135
|
+
| **Work** — produces code, a report, a decision | One hub assigns, workers go quiet and deliver | hub `all`, workers `mention` | Echoes, jumping ahead, duplicate reports, thinking out loud in the room |
|
|
136
|
+
| **Roleplay / companionship** — the owner watches or joins characters | Characters pick up each other's lines and stay in character, **carrying on without a human** | everyone `all`; `batch_delay_seconds` is pacing; no hub, no round limit | Repetition, breaking character, playing different scenes, flooding too fast |
|
|
137
|
+
| **Roundtable / review** — argue a question to a conclusion | Diverge first, then a chair converges and writes the conclusion | chair `all` + short delay; others `all` + long delay | Going quiet after one round (cut off, not converged); nobody writes the conclusion |
|
|
138
|
+
|
|
139
|
+
**Never carry a work group's rules into the other two.** "Only speak when @-ed"
|
|
140
|
+
kills a scene — characters are picking up a line, not taking a ticket.
|
|
141
|
+
"Stop after three rounds" truncates a discussion instead of converging it.
|
|
142
|
+
|
|
143
|
+
### Then, in any group
|
|
144
|
+
|
|
145
|
+
1. **Hard before soft.** When something is going wrong, the speaking settings
|
|
146
|
+
(`muted` / `reply_mode` / `batch_delay_seconds`) are the tourniquet; prompts
|
|
147
|
+
are the follow-up. A soft rule in a prompt stops working after compaction, a
|
|
148
|
+
restart, or a long session — pair every soft rule with a hard one.
|
|
149
|
+
2. **Smallest change, one agent at a time.** The owner has to be able to follow
|
|
150
|
+
what you changed. Read the result before changing anything else.
|
|
151
|
+
3. **The owner's "stop" is a hard stop** in every kind of group.
|
|
152
|
+
4. **Say what you cannot do.** You cannot change an agent's runtime, restart it,
|
|
153
|
+
or clear its context from here. Say so rather than appearing to try.
|
|
154
|
+
|
|
155
|
+
## Errors, and when to stop
|
|
156
|
+
|
|
157
|
+
All responses are **HTTP 200**; the business code is in the envelope's `code`
|
|
158
|
+
field. A 200 is not by itself success.
|
|
159
|
+
|
|
160
|
+
| Code | Means | Do |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `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 |
|
|
163
|
+
| `403` | Insufficient scope — the `agent:orchestrate` scope is missing (rare; it is a default scope) | Report. Do not retry |
|
|
164
|
+
| `401` | Credentials stale or revoked | Report once. Do not loop |
|
|
165
|
+
| `16025` | Connect-code rate limit; the bucket is your owner's, shared with their own manual issuance | Wait. Do not hammer it |
|
|
166
|
+
| `400` | Malformed body, or a malformed/wrong-prefix id | Fix the shape. Do not resend unchanged |
|
|
167
|
+
| `29002` | Target agent is not the owner's, **or does not exist** | Report. The two are folded on purpose — do not infer existence, do not probe other ids |
|
|
168
|
+
| `29003` | Group is not one the owner administers, **or does not exist** | Same |
|
|
169
|
+
| `29005` | Connect code not found **or not the owner's** | Same |
|
|
170
|
+
| `29004` | Input rejected; the message says what is wrong | Read it and fix the input. Do not resend unchanged |
|
|
171
|
+
| `29001` | No agent identity on the call | Report; this is a configuration fault, not something to retry |
|
|
172
|
+
|
|
173
|
+
## Verification
|
|
174
|
+
|
|
175
|
+
After a write, re-read the thing you wrote (`GET` the agent or the group) and
|
|
176
|
+
tell the owner what it says now — not what you sent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-set-greeting
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
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
|
|
|
@@ -12,8 +12,15 @@ built-in instruction telling you to send a short, friendly self-introduction.
|
|
|
12
12
|
|
|
13
13
|
You can override that instruction with a file at **`~/clawchat/greeting.md`** (the
|
|
14
14
|
`clawchat` folder in the current user's home directory). When that file exists and is
|
|
15
|
-
non-empty, the plugin uses its content
|
|
16
|
-
|
|
15
|
+
non-empty, the plugin uses its content as the **body** of the instruction on the next
|
|
16
|
+
activation / first connect, in place of the built-in wording.
|
|
17
|
+
|
|
18
|
+
It is a **partial** override, not a full replacement of everything the plugin sends: the
|
|
19
|
+
plugin may still append a short trailing line of its own after your text. Today that is a
|
|
20
|
+
`Reply in <Language>.` line, added when the owner's ClawChat app language is known (and
|
|
21
|
+
omitted entirely when it is not). That line says which language to answer in, not what to
|
|
22
|
+
say, so it is not something your override replaces — write your instruction as the *what*,
|
|
23
|
+
and do not try to cancel or contradict the trailing line from inside the file.
|
|
17
24
|
|
|
18
25
|
## Important: the file is a prompt to YOU, not a literal message
|
|
19
26
|
|
|
@@ -36,7 +43,8 @@ one sentence." — not the finished greeting sentence itself.
|
|
|
36
43
|
## Resetting to the default
|
|
37
44
|
|
|
38
45
|
To restore the built-in greeting, delete `~/clawchat/greeting.md` (or empty it). With the
|
|
39
|
-
file absent or empty, the plugin falls back to its built-in greeting instruction
|
|
46
|
+
file absent or empty, the plugin falls back to its built-in greeting instruction body (and
|
|
47
|
+
still appends the same trailing line it would otherwise).
|
|
40
48
|
|
|
41
49
|
## The other greeting: first message to a new friend
|
|
42
50
|
|
|
@@ -47,7 +55,8 @@ AI agent acting on behalf of your owner, and invite them to say what they need
|
|
|
47
55
|
to share the owner's private information.
|
|
48
56
|
|
|
49
57
|
Override it the same way with **`~/clawchat/friend-greeting.md`**: same rules as above (it
|
|
50
|
-
is an instruction to you, not the literal message; keep it short; no secrets
|
|
58
|
+
is an instruction to you, not the literal message; keep it short; no secrets; it is a
|
|
59
|
+
partial override that the plugin may still append its own trailing line to). Delete or
|
|
51
60
|
empty the file to restore the built-in instruction. The owner can turn this greeting off
|
|
52
61
|
entirely in the plugin config (`friend_greeting: false` for Hermes, `friendGreeting: false`
|
|
53
62
|
for OpenClaw); it is not something you can disable from chat.
|
package/skills/manifest.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"skills": {
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"clawchat-core": {
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.7.0",
|
|
7
7
|
"path": "openclaw/clawchat-core/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
8
|
+
"sha256": "16799ac6db488e6aa0aef650d61b629479b5aaf44be8acf8d36e6e7a5954698c",
|
|
9
|
+
"bytes": 15316
|
|
10
10
|
},
|
|
11
11
|
"clawchat-liveware": {
|
|
12
12
|
"version": "1.2.2",
|
|
@@ -21,24 +21,30 @@
|
|
|
21
21
|
"bytes": 8892
|
|
22
22
|
},
|
|
23
23
|
"clawchat-set-greeting": {
|
|
24
|
-
"version": "1.
|
|
24
|
+
"version": "1.2.0",
|
|
25
25
|
"path": "shared/clawchat-set-greeting/SKILL.md",
|
|
26
|
-
"sha256": "
|
|
27
|
-
"bytes":
|
|
26
|
+
"sha256": "bf1bbb71d287faafa499920f91450e99c27f638e7ee67bfb209a43418937a87c",
|
|
27
|
+
"bytes": 4184
|
|
28
28
|
},
|
|
29
29
|
"clawchat-liveware-sample": {
|
|
30
30
|
"version": "2.0.0",
|
|
31
31
|
"path": "openclaw/clawchat-liveware-sample/SKILL.md",
|
|
32
32
|
"sha256": "edafa5f802907c97c32af63feb5b62dcbb41c064649912c710808d27187aaa57",
|
|
33
33
|
"bytes": 13938
|
|
34
|
+
},
|
|
35
|
+
"clawchat-orchestration": {
|
|
36
|
+
"version": "1.1.0",
|
|
37
|
+
"path": "shared/clawchat-orchestration/SKILL.md",
|
|
38
|
+
"sha256": "b3039b5d19387e419e4393e59251b4c6dfa208d1e1e5c827221f71c49e8d6bc2",
|
|
39
|
+
"bytes": 9765
|
|
34
40
|
}
|
|
35
41
|
},
|
|
36
42
|
"hermes": {
|
|
37
43
|
"clawchat-core": {
|
|
38
|
-
"version": "1.
|
|
44
|
+
"version": "1.13.0",
|
|
39
45
|
"path": "hermes/clawchat-core/SKILL.md",
|
|
40
|
-
"sha256": "
|
|
41
|
-
"bytes":
|
|
46
|
+
"sha256": "ebbb82dc3062e8dfa85b55ac9cb708c3b9c5b694cb2ea5bfcd03f997ee1efa01",
|
|
47
|
+
"bytes": 22529
|
|
42
48
|
},
|
|
43
49
|
"clawchat-liveware": {
|
|
44
50
|
"version": "1.2.2",
|
|
@@ -53,16 +59,22 @@
|
|
|
53
59
|
"bytes": 8892
|
|
54
60
|
},
|
|
55
61
|
"clawchat-set-greeting": {
|
|
56
|
-
"version": "1.
|
|
62
|
+
"version": "1.2.0",
|
|
57
63
|
"path": "shared/clawchat-set-greeting/SKILL.md",
|
|
58
|
-
"sha256": "
|
|
59
|
-
"bytes":
|
|
64
|
+
"sha256": "bf1bbb71d287faafa499920f91450e99c27f638e7ee67bfb209a43418937a87c",
|
|
65
|
+
"bytes": 4184
|
|
60
66
|
},
|
|
61
67
|
"clawchat-liveware-sample": {
|
|
62
68
|
"version": "2.0.0",
|
|
63
69
|
"path": "hermes/clawchat-liveware-sample/SKILL.md",
|
|
64
70
|
"sha256": "8278db2646d1553e41f4cf061c710b5363c3f41fa9ae32d8b1021fa2175b9a41",
|
|
65
71
|
"bytes": 13838
|
|
72
|
+
},
|
|
73
|
+
"clawchat-orchestration": {
|
|
74
|
+
"version": "1.1.0",
|
|
75
|
+
"path": "shared/clawchat-orchestration/SKILL.md",
|
|
76
|
+
"sha256": "b3039b5d19387e419e4393e59251b4c6dfa208d1e1e5c827221f71c49e8d6bc2",
|
|
77
|
+
"bytes": 9765
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { languageDisplayName, type OwnerLanguage } from "./owner-language.ts";
|
|
4
5
|
|
|
5
6
|
// Worded to make the greeting the single required action. Earlier wording
|
|
6
7
|
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
@@ -17,13 +18,30 @@ const ACTIVATION_BOOTSTRAP_FALLBACK = [
|
|
|
17
18
|
|
|
18
19
|
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
19
20
|
// first-load without a restart. Any read failure falls back to the built-in text.
|
|
20
|
-
|
|
21
|
+
// The override does NOT fully replace the built-in text: when the owner's
|
|
22
|
+
// language is known a trailing instruction is appended (see below), because the
|
|
23
|
+
// override says what to say, not which language to say it in — that's a
|
|
24
|
+
// cross-cutting concern the user did not opt out of by supplying their own
|
|
25
|
+
// greeting.
|
|
26
|
+
//
|
|
27
|
+
// `language` is nullable and defaults to null on purpose. The owner-profile
|
|
28
|
+
// cache is cold on a fresh install, and this prompt is dispatched immediately
|
|
29
|
+
// on connect without awaiting it, so the caller often has NO locale. Asserting
|
|
30
|
+
// "Reply in English." to an owner who never said they speak English is worse
|
|
31
|
+
// than asserting nothing — with no line the model infers the language from
|
|
32
|
+
// context, which is what it did before this feature existed. So: unknown
|
|
33
|
+
// language ⇒ no line at all.
|
|
34
|
+
export function buildActivationBootstrapText(
|
|
35
|
+
homeDir: string = os.homedir(),
|
|
36
|
+
language: OwnerLanguage | null = null,
|
|
37
|
+
): string {
|
|
21
38
|
const greetingPath = path.join(homeDir, "clawchat", "greeting.md");
|
|
39
|
+
let base = ACTIVATION_BOOTSTRAP_FALLBACK;
|
|
22
40
|
try {
|
|
23
41
|
const raw = fs.readFileSync(greetingPath);
|
|
24
42
|
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
25
43
|
if (override.length > 0) {
|
|
26
|
-
|
|
44
|
+
base = override;
|
|
27
45
|
}
|
|
28
46
|
} catch (error) {
|
|
29
47
|
const code = (error as NodeJS.ErrnoException).code;
|
|
@@ -31,5 +49,6 @@ export function buildActivationBootstrapText(homeDir: string = os.homedir()): st
|
|
|
31
49
|
console.warn(`clawchat.greeting failed to read override ${greetingPath}:`, error);
|
|
32
50
|
}
|
|
33
51
|
}
|
|
34
|
-
return
|
|
52
|
+
if (language === null) return base;
|
|
53
|
+
return `${base}\n\nReply in ${languageDisplayName(language)}.`;
|
|
35
54
|
}
|
package/src/friend-greeting.ts
CHANGED
|
@@ -17,6 +17,7 @@ import os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { EVENT, type Envelope } from "./protocol-types.ts";
|
|
19
19
|
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
20
|
+
import { languageDisplayName, type OwnerLanguage } from "./owner-language.ts";
|
|
20
21
|
|
|
21
22
|
export const FRIEND_GREETING_FALLBACK = [
|
|
22
23
|
"A ClawChat user has just become your friend. You are now in a direct conversation with them; they are not your owner.",
|
|
@@ -27,14 +28,23 @@ export const FRIEND_GREETING_FALLBACK = [
|
|
|
27
28
|
|
|
28
29
|
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
29
30
|
// friend without a restart. Any read failure falls back to the built-in text.
|
|
30
|
-
// Mirrors `buildActivationBootstrapText` (`~/clawchat/greeting.md`)
|
|
31
|
-
|
|
31
|
+
// Mirrors `buildActivationBootstrapText` (`~/clawchat/greeting.md`), including
|
|
32
|
+
// its nullable `language`: the override does NOT fully replace the built-in
|
|
33
|
+
// text — when the owner's language is known a trailing instruction is appended
|
|
34
|
+
// (see below), because the override says what to say, not which language to say
|
|
35
|
+
// it in. When it is NOT known the line is omitted entirely rather than guessed;
|
|
36
|
+
// see `buildActivationBootstrapText` for why "en" is the wrong guess here.
|
|
37
|
+
export function buildFriendGreetingText(
|
|
38
|
+
homeDir: string = os.homedir(),
|
|
39
|
+
language: OwnerLanguage | null = null,
|
|
40
|
+
): string {
|
|
32
41
|
const greetingPath = path.join(homeDir, "clawchat", "friend-greeting.md");
|
|
42
|
+
let base = FRIEND_GREETING_FALLBACK;
|
|
33
43
|
try {
|
|
34
44
|
const raw = fs.readFileSync(greetingPath);
|
|
35
45
|
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
36
46
|
if (override.length > 0) {
|
|
37
|
-
|
|
47
|
+
base = override;
|
|
38
48
|
}
|
|
39
49
|
} catch (error) {
|
|
40
50
|
const code = (error as NodeJS.ErrnoException).code;
|
|
@@ -42,7 +52,8 @@ export function buildFriendGreetingText(homeDir: string = os.homedir()): string
|
|
|
42
52
|
console.warn(`clawchat.friend-greeting failed to read override ${greetingPath}:`, error);
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
|
-
return
|
|
55
|
+
if (language === null) return base;
|
|
56
|
+
return `${base}\n\nReply in ${languageDisplayName(language)}.`;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export interface BuildFriendGreetingEnvelopeParams {
|
|
@@ -51,6 +62,22 @@ export interface BuildFriendGreetingEnvelopeParams {
|
|
|
51
62
|
conversationId: string;
|
|
52
63
|
/** The new friend's `usr_…` id — becomes the sender so the turn's session and sender metadata resolve to them. */
|
|
53
64
|
friendUserId: string;
|
|
65
|
+
/**
|
|
66
|
+
* Language the greeting should reply in, or `null` when the owner's locale
|
|
67
|
+
* is not known yet — in which case no language line is emitted at all.
|
|
68
|
+
*
|
|
69
|
+
* Required, not optional: the activation-greeting equivalent is required,
|
|
70
|
+
* and an optional one here would let a future call site silently omit it and
|
|
71
|
+
* re-acquire the default that item 1 of the cross-repo review removed. Pass
|
|
72
|
+
* an explicit `null` to mean "unknown".
|
|
73
|
+
*
|
|
74
|
+
* The recipient here is the new friend, NOT the owner — but we deliberately
|
|
75
|
+
* use the OWNER's language anyway: friends are usually in the same language
|
|
76
|
+
* circle, and the backend only exposes `locale` on the owner profile
|
|
77
|
+
* endpoint, so no third-party locale is obtainable. Do not "fix" this into a
|
|
78
|
+
* friend-specific lookup.
|
|
79
|
+
*/
|
|
80
|
+
language: OwnerLanguage | null;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/**
|
|
@@ -60,8 +87,8 @@ export interface BuildFriendGreetingEnvelopeParams {
|
|
|
60
87
|
* refused at the outbound boundary after a full LLM turn has been spent.
|
|
61
88
|
*/
|
|
62
89
|
export function buildFriendGreetingEnvelope(params: BuildFriendGreetingEnvelopeParams): Envelope {
|
|
63
|
-
const { account, conversationId, friendUserId } = params;
|
|
64
|
-
const text = buildFriendGreetingText();
|
|
90
|
+
const { account, conversationId, friendUserId, language } = params;
|
|
91
|
+
const text = buildFriendGreetingText(undefined, language);
|
|
65
92
|
const now = Date.now();
|
|
66
93
|
return {
|
|
67
94
|
version: "2",
|
package/src/liveware-sample.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { LivewareSampleRow, LivewareSampleUpsert } from "./storage.ts";
|
|
|
14
14
|
import { livewareCliEnv, type LivewareLogger } from "./liveware-cli.ts";
|
|
15
15
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
16
16
|
import { normalizeOpenclawClawlingAccountId } from "./config.ts";
|
|
17
|
+
import { resolveOwnerLanguage, type OwnerLanguage } from "./owner-language.ts";
|
|
17
18
|
|
|
18
19
|
export const LIVEWARES_TARGET = "openclaw";
|
|
19
20
|
export const LIVEWARE_SAMPLE_ID = "liveware-sample";
|
|
@@ -712,6 +713,10 @@ export type LivewareSampleSupervisorDeps = {
|
|
|
712
713
|
listApps: () => Promise<{ apps: { app_id?: string }[] }>;
|
|
713
714
|
registerApp: (p: { name: string; appId: string; url: string }) => Promise<unknown>;
|
|
714
715
|
notifyOwner: (text: string) => Promise<boolean>;
|
|
716
|
+
/** The owner's reported app locale (`agent_owner_locale`), read lazily:
|
|
717
|
+
* the intro can be retried for ~10 minutes, by which time a locale that
|
|
718
|
+
* was absent at construction may have landed. */
|
|
719
|
+
resolveOwnerLocale?: () => string | null;
|
|
715
720
|
fetchFn: FetchLike;
|
|
716
721
|
ref?: string;
|
|
717
722
|
spawnFn?: SpawnLike;
|
|
@@ -726,11 +731,61 @@ export type LivewareSampleSupervisorDeps = {
|
|
|
726
731
|
setTimeoutFn?: typeof setTimeout;
|
|
727
732
|
};
|
|
728
733
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
+
/** Last-resort copy when the content tree's table cannot be read. English only:
|
|
735
|
+
* a second full translation set in-process would be a competing source of truth. */
|
|
736
|
+
export const LIVEWARE_SAMPLE_INTRO_FALLBACK_EN =
|
|
737
|
+
"I've installed a liveware demo app for you — \"Liveware Sample\". " +
|
|
738
|
+
"To open it: in our chat, tap the Apps button (✦) in the top-right corner, " +
|
|
739
|
+
"then pick \"Liveware Sample\" in the panel. " +
|
|
740
|
+
"The page has a full walkthrough — try saying to me: Change the title to Hello Liveware. " +
|
|
741
|
+
"I can also see the buttons you tap and the notes you submit there, so just ask me anytime.";
|
|
742
|
+
|
|
743
|
+
/** Read the localized intro from the installed sample's `intro.i18n.json`.
|
|
744
|
+
* Any failure returns the English fallback — delivery must never be blocked
|
|
745
|
+
* by a content-tree problem.
|
|
746
|
+
*
|
|
747
|
+
* Every one of the four degradation modes (missing file / corrupt JSON /
|
|
748
|
+
* missing key / empty value) warns first. Falling back silently means a
|
|
749
|
+
* mistake in the content tree ships English to every non-English owner with
|
|
750
|
+
* zero signal anywhere; the fallback is the safety net, not the plan. Kept
|
|
751
|
+
* symmetric with `load_intro_text` in the hermes plugin. */
|
|
752
|
+
export function loadIntroText(appDir: string, language: OwnerLanguage): string {
|
|
753
|
+
const tablePath = path.join(appDir, "intro.i18n.json");
|
|
754
|
+
let raw: string;
|
|
755
|
+
try {
|
|
756
|
+
raw = fs.readFileSync(tablePath, "utf8");
|
|
757
|
+
} catch (error) {
|
|
758
|
+
console.warn(
|
|
759
|
+
`clawchat.liveware-sample intro table unreadable at ${tablePath}; using the English fallback:`,
|
|
760
|
+
error,
|
|
761
|
+
);
|
|
762
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
763
|
+
}
|
|
764
|
+
let value: unknown;
|
|
765
|
+
try {
|
|
766
|
+
const table = JSON.parse(raw) as { intro?: Record<string, unknown> } | null;
|
|
767
|
+
value = table?.intro?.[language];
|
|
768
|
+
} catch (error) {
|
|
769
|
+
console.warn(
|
|
770
|
+
`clawchat.liveware-sample intro table at ${tablePath} is not valid JSON; using the English fallback:`,
|
|
771
|
+
error,
|
|
772
|
+
);
|
|
773
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
774
|
+
}
|
|
775
|
+
if (typeof value !== "string") {
|
|
776
|
+
console.warn(
|
|
777
|
+
`clawchat.liveware-sample intro table at ${tablePath} has no "${language}" entry; using the English fallback`,
|
|
778
|
+
);
|
|
779
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
780
|
+
}
|
|
781
|
+
if (value.trim().length === 0) {
|
|
782
|
+
console.warn(
|
|
783
|
+
`clawchat.liveware-sample intro table at ${tablePath} has an empty "${language}" entry; using the English fallback`,
|
|
784
|
+
);
|
|
785
|
+
return LIVEWARE_SAMPLE_INTRO_FALLBACK_EN;
|
|
786
|
+
}
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
734
789
|
|
|
735
790
|
const DEFAULT_SAMPLE_PORT = 43110;
|
|
736
791
|
const RESTART_WINDOW_MS = 30 * 60 * 1000;
|
|
@@ -1153,7 +1208,8 @@ export class LivewareSampleSupervisor {
|
|
|
1153
1208
|
const { deps } = this;
|
|
1154
1209
|
let delivered = false;
|
|
1155
1210
|
try {
|
|
1156
|
-
|
|
1211
|
+
const language = resolveOwnerLanguage(deps.resolveOwnerLocale?.() ?? null);
|
|
1212
|
+
delivered = await deps.notifyOwner(loadIntroText(path.join(deps.sampleRoot, "app"), language));
|
|
1157
1213
|
} catch (err) {
|
|
1158
1214
|
deps.log?.debug?.(`liveware-sample: intro send error: ${String(err)}`);
|
|
1159
1215
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the owner's reported app language into one of the six languages the
|
|
3
|
+
* ClawChat content tree carries. `agent_owner_locale` comes from the owner's
|
|
4
|
+
* ClawChat app setting and is omitted entirely when they never reported one —
|
|
5
|
+
* every unresolvable case lands on English rather than guessing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type OwnerLanguage = "en" | "es" | "ja" | "ko" | "zh" | "zh_Hant";
|
|
9
|
+
|
|
10
|
+
export const OWNER_LANGUAGES: readonly OwnerLanguage[] = [
|
|
11
|
+
"en", "es", "ja", "ko", "zh", "zh_Hant",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const DISPLAY_NAMES: Record<OwnerLanguage, string> = {
|
|
15
|
+
en: "English",
|
|
16
|
+
es: "Spanish",
|
|
17
|
+
ja: "Japanese",
|
|
18
|
+
ko: "Korean",
|
|
19
|
+
zh: "Simplified Chinese",
|
|
20
|
+
zh_Hant: "Traditional Chinese",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Traditional-Chinese regions must be tested before the bare `zh` prefix,
|
|
24
|
+
// because they also start with "zh".
|
|
25
|
+
const TRADITIONAL_PREFIXES = ["zh-hant", "zh-tw", "zh-hk", "zh-mo"];
|
|
26
|
+
|
|
27
|
+
export function resolveOwnerLanguage(locale?: string | null): OwnerLanguage {
|
|
28
|
+
const tag = String(locale ?? "").trim().toLowerCase().replace(/_/g, "-");
|
|
29
|
+
if (!tag) return "en";
|
|
30
|
+
if (TRADITIONAL_PREFIXES.some((p) => tag.startsWith(p))) return "zh_Hant";
|
|
31
|
+
if (tag.startsWith("zh")) return "zh";
|
|
32
|
+
if (tag.startsWith("ja")) return "ja";
|
|
33
|
+
if (tag.startsWith("ko")) return "ko";
|
|
34
|
+
if (tag.startsWith("es")) return "es";
|
|
35
|
+
return "en";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function languageDisplayName(lang: OwnerLanguage): string {
|
|
39
|
+
return DISPLAY_NAMES[lang];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a locale ONLY when one was actually reported; `null` otherwise.
|
|
44
|
+
*
|
|
45
|
+
* `resolveOwnerLanguage` must answer with a language because the intro lookup
|
|
46
|
+
* has to pick one copy and `en` is the documented default. The greeting path
|
|
47
|
+
* is the opposite case: the language line is an explicit assertion to the
|
|
48
|
+
* model, and the owner-profile cache is COLD on a fresh install (the
|
|
49
|
+
* activation row is written without a locale and the bootstrap fires
|
|
50
|
+
* immediately on connect). Collapsing "unknown" into "en" there tells a
|
|
51
|
+
* Chinese owner's agent to reply in English. Callers on the greeting path use
|
|
52
|
+
* this and omit the line entirely when it returns null.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveOwnerLanguageIfKnown(locale?: string | null): OwnerLanguage | null {
|
|
55
|
+
return String(locale ?? "").trim() ? resolveOwnerLanguage(locale) : null;
|
|
56
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from ".
|
|
|
18
18
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
19
19
|
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
20
20
|
import { buildFriendGreetingEnvelope } from "./friend-greeting.ts";
|
|
21
|
+
import { resolveOwnerLanguageIfKnown, type OwnerLanguage } from "./owner-language.ts";
|
|
21
22
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
22
23
|
import { readOnboardingReport } from "./onboarding-report.ts";
|
|
23
24
|
import path from "node:path";
|
|
@@ -139,6 +140,7 @@ type RuntimeConnectionStore = Pick<
|
|
|
139
140
|
| "listRecentGroupMessages"
|
|
140
141
|
| "recallMessage"
|
|
141
142
|
| "upsertOwnerProfile"
|
|
143
|
+
| "getOwnerProfile"
|
|
142
144
|
>
|
|
143
145
|
>;
|
|
144
146
|
|
|
@@ -537,8 +539,10 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
537
539
|
function buildActivationBootstrapEnvelope(params: {
|
|
538
540
|
account: ResolvedOpenclawClawlingAccount;
|
|
539
541
|
conversationId: string;
|
|
542
|
+
/** `null` when the owner's locale is not known — then no language line is emitted. */
|
|
543
|
+
language: OwnerLanguage | null;
|
|
540
544
|
}): Envelope {
|
|
541
|
-
const text = buildActivationBootstrapText();
|
|
545
|
+
const text = buildActivationBootstrapText(undefined, params.language);
|
|
542
546
|
const now = Date.now();
|
|
543
547
|
return {
|
|
544
548
|
version: "2",
|
|
@@ -1235,6 +1239,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1235
1239
|
return await client.registerApp(p);
|
|
1236
1240
|
},
|
|
1237
1241
|
notifyOwner: (text) => livewareSampleNotify(text),
|
|
1242
|
+
resolveOwnerLocale: () => store.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
1238
1243
|
fetchFn: globalThis.fetch,
|
|
1239
1244
|
log: {
|
|
1240
1245
|
warn: (m) => log?.info?.(m),
|
|
@@ -2645,8 +2650,20 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2645
2650
|
log?.info?.(
|
|
2646
2651
|
`[${accountId}] clawchat-plugin-openclaw friend greeting dispatch friend=${friendUserId} chat_id=${conversationId}`,
|
|
2647
2652
|
);
|
|
2653
|
+
// The recipient here is the new friend, NOT the owner — but we
|
|
2654
|
+
// deliberately use the OWNER's language anyway: friends are usually in
|
|
2655
|
+
// the same language circle, and the backend only exposes `locale` on
|
|
2656
|
+
// the owner profile endpoint, so no third-party locale is obtainable.
|
|
2657
|
+
//
|
|
2658
|
+
// `…IfKnown`, not `resolveOwnerLanguage`: this is a synchronous read of a
|
|
2659
|
+
// cache that may never have been filled, and it must NOT be awaited here.
|
|
2660
|
+
// A cold cache yields null and the greeting simply carries no language
|
|
2661
|
+
// line — see `buildActivationBootstrapText`.
|
|
2662
|
+
const language = resolveOwnerLanguageIfKnown(
|
|
2663
|
+
store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
2664
|
+
);
|
|
2648
2665
|
await handleInboundEnvelope(
|
|
2649
|
-
buildFriendGreetingEnvelope({ account, conversationId, friendUserId }),
|
|
2666
|
+
buildFriendGreetingEnvelope({ account, conversationId, friendUserId, language }),
|
|
2650
2667
|
);
|
|
2651
2668
|
})().catch((err) => {
|
|
2652
2669
|
log?.error?.(
|
|
@@ -3623,8 +3640,21 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3623
3640
|
claimedInFlight = true;
|
|
3624
3641
|
incrementActivationBootstrapInFlight(accountId);
|
|
3625
3642
|
bootstrapGreetingDelivered = false;
|
|
3643
|
+
// Synchronous, un-awaited read: the bootstrap is dispatched the moment
|
|
3644
|
+
// the connection comes up (`void dispatchActivationBootstrap()`), so on a
|
|
3645
|
+
// fresh install `owner_profile` has not been pulled yet and there is no
|
|
3646
|
+
// locale to be had. `…IfKnown` returns null there and the prompt ships
|
|
3647
|
+
// with no language line, instead of hard-asserting English at an owner
|
|
3648
|
+
// who may not speak it. Do NOT add an await here to reach the locale.
|
|
3649
|
+
const language = resolveOwnerLanguageIfKnown(
|
|
3650
|
+
store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
3651
|
+
);
|
|
3626
3652
|
const result = await handleInboundEnvelope(
|
|
3627
|
-
buildActivationBootstrapEnvelope({
|
|
3653
|
+
buildActivationBootstrapEnvelope({
|
|
3654
|
+
account,
|
|
3655
|
+
conversationId: claimedBootstrap.conversationId,
|
|
3656
|
+
language,
|
|
3657
|
+
}),
|
|
3628
3658
|
);
|
|
3629
3659
|
if (result !== "submitted") {
|
|
3630
3660
|
settleBootstrap("turn was not submitted");
|
package/src/skill-update.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const OFFICIAL_SKILLS_BASE =
|
|
|
71
71
|
* in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
|
|
72
72
|
* imports the same ref, so the `livewares` tree at that tag is pinned too.
|
|
73
73
|
*/
|
|
74
|
-
export const DEFAULT_SKILLS_REF = "skills-v1.
|
|
74
|
+
export const DEFAULT_SKILLS_REF = "skills-v1.13.0";
|
|
75
75
|
|
|
76
76
|
/** Refuse to treat an absurdly large response as a skill file (defence in depth). */
|
|
77
77
|
export const MAX_SKILL_BYTES = 256 * 1024;
|