@clawling/clawchat-plugin-openclaw 2026.9.17-1 → 2026.9.19-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.
@@ -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
- export function buildActivationBootstrapText(homeDir = os.homedir()) {
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
- return override;
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
- return ACTIVATION_BOOTSTRAP_FALLBACK;
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
- export function buildFriendGreetingText(homeDir = os.homedir()) {
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
- return override;
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
- return FRIEND_GREETING_FALLBACK;
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
- export const LIVEWARE_SAMPLE_INTRO_TEXT = "我给你安装了一个 liveware 演示应用「Liveware Sample」。" +
572
- "入口:在我们的对话页面,点右上角的「应用」按钮(✦),在打开的面板里选名为「Liveware Sample」的应用。" +
573
- "页面上有完整的使用引导,试试对我说:把标题改成 Hello Liveware。" +
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
- delivered = await deps.notifyOwner(LIVEWARE_SAMPLE_INTRO_TEXT);
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
+ }
@@ -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
- await handleInboundEnvelope(buildFriendGreetingEnvelope({ account, conversationId, friendUserId }));
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
- const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
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;
@@ -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.10.0";
69
+ export const DEFAULT_SKILLS_REF = "skills-v1.12.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.9.17-1",
3
+ "version": "2026.9.19-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clawchat-core
3
- version: 1.4.0
3
+ version: 1.5.0
4
4
  description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, sending an image, file, or voice/audio clip into a conversation, or plugin install/update/activation.
5
5
  ---
6
6
 
@@ -129,6 +129,8 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
129
129
 
130
130
  ## Profile And Identity Sync
131
131
 
132
+ **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`.
133
+
132
134
  When updating the OpenClaw agent identity file, such as `SOUL.md` or `soul.md`, also update the configured ClawChat account profile when the changed field is shown on the ClawChat profile:
133
135
 
134
136
  ```mermaid
@@ -3,10 +3,10 @@
3
3
  "skills": {
4
4
  "openclaw": {
5
5
  "clawchat-core": {
6
- "version": "1.4.0",
6
+ "version": "1.5.0",
7
7
  "path": "openclaw/clawchat-core/SKILL.md",
8
- "sha256": "c0cd2a83d6b48b6f4778a2127b5f4f7dd1ba33ea093972e714eee4bc0af6ed67",
9
- "bytes": 12893
8
+ "sha256": "911e18c019a13d65174a7fc591a1f862fd3de1b1f461bca4a6d2b703f2b82bd6",
9
+ "bytes": 13624
10
10
  },
11
11
  "clawchat-liveware": {
12
12
  "version": "1.2.2",
@@ -35,10 +35,10 @@
35
35
  },
36
36
  "hermes": {
37
37
  "clawchat-core": {
38
- "version": "1.10.0",
38
+ "version": "1.11.0",
39
39
  "path": "hermes/clawchat-core/SKILL.md",
40
- "sha256": "6130e98e426f5d51392fd5bd7d75932d859b0531ea4216ef9a0a50526b99dfb6",
41
- "bytes": 20124
40
+ "sha256": "fadc82be5cfd43d6ccedcf7ec9b3062fa15f6eeea9fd556f4c35c3f9488e22a1",
41
+ "bytes": 20784
42
42
  },
43
43
  "clawchat-liveware": {
44
44
  "version": "1.2.2",
@@ -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
- export function buildActivationBootstrapText(homeDir: string = os.homedir()): string {
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
- return override;
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 ACTIVATION_BOOTSTRAP_FALLBACK;
52
+ if (language === null) return base;
53
+ return `${base}\n\nReply in ${languageDisplayName(language)}.`;
35
54
  }
@@ -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
- export function buildFriendGreetingText(homeDir: string = os.homedir()): string {
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
- return override;
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 FRIEND_GREETING_FALLBACK;
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",
@@ -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
- export const LIVEWARE_SAMPLE_INTRO_TEXT =
730
- "我给你安装了一个 liveware 演示应用「Liveware Sample」。" +
731
- "入口:在我们的对话页面,点右上角的「应用」按钮(✦),在打开的面板里选名为「Liveware Sample」的应用。" +
732
- "页面上有完整的使用引导,试试对我说:把标题改成 Hello Liveware。" +
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
- delivered = await deps.notifyOwner(LIVEWARE_SAMPLE_INTRO_TEXT);
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({ account, conversationId: claimedBootstrap.conversationId }),
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");
@@ -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.10.0";
74
+ export const DEFAULT_SKILLS_REF = "skills-v1.12.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;