From d750477d4352b9af77e71e4ef096ffde192b7911 Mon Sep 17 00:00:00 2001
From: lzyyzznl
Date: Thu, 9 Jul 2026 19:38:20 +0800
Subject: [PATCH] fix(migrate-claude): use truncateUtf16Safe for skill
description truncation (#102616)
* fix(migrate-claude): use truncateUtf16Safe for skill description truncation
One .slice(0, 180) truncation site in the migrate-claude extension
may cut UTF-16 surrogate pairs in half when the skill description
contains multi-byte characters such as emoji. Replace it with
truncateUtf16Safe to keep the truncated output valid Unicode.
* test(migrate-claude): cover UTF-16 skill description
---------
Co-authored-by: Peter Steinberger
---
extensions/migrate-claude/provider.test.ts | 16 ++++++++++++----
extensions/migrate-claude/skills.ts | 3 ++-
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/extensions/migrate-claude/provider.test.ts b/extensions/migrate-claude/provider.test.ts
index 01bb4440f6d..7d8167f883a 100644
--- a/extensions/migrate-claude/provider.test.ts
+++ b/extensions/migrate-claude/provider.test.ts
@@ -133,7 +133,11 @@ describe("Claude migration provider", () => {
},
}),
);
- await writeFile(path.join(source, ".claude", "commands", "ship.md"), "Ship $ARGUMENTS\n");
+ const commandDescriptionPrefix = "a".repeat(179);
+ await writeFile(
+ path.join(source, ".claude", "commands", "ship.md"),
+ `${commandDescriptionPrefix}😀tail\n`,
+ );
await writeFile(path.join(source, ".claude", "skills", "Review", "SKILL.md"), "# Review\n");
const config = {
@@ -169,9 +173,13 @@ describe("Claude migration provider", () => {
expect(await fs.readFile(path.join(workspaceDir, "AGENTS.md"), "utf8")).toContain(
"Imported from Claude: project CLAUDE.md",
);
- await expect(
- fs.access(path.join(workspaceDir, "skills", "claude-command-ship", "SKILL.md")),
- ).resolves.toBeUndefined();
+ const generatedSkill = await fs.readFile(
+ path.join(workspaceDir, "skills", "claude-command-ship", "SKILL.md"),
+ "utf8",
+ );
+ expect(generatedSkill.split("\n").find((line) => line.startsWith("description: "))).toBe(
+ `description: ${JSON.stringify(commandDescriptionPrefix)}`,
+ );
await expect(
fs.access(path.join(workspaceDir, "skills", "review", "SKILL.md")),
).resolves.toBeUndefined();
diff --git a/extensions/migrate-claude/skills.ts b/extensions/migrate-claude/skills.ts
index 792928b124f..3db062dd4f4 100644
--- a/extensions/migrate-claude/skills.ts
+++ b/extensions/migrate-claude/skills.ts
@@ -9,6 +9,7 @@ import {
MIGRATION_REASON_TARGET_EXISTS,
} from "openclaw/plugin-sdk/migration";
import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry";
+import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { exists, readText, sanitizeName } from "./helpers.js";
import type { ClaudeSource } from "./source.js";
import type { PlannedTargets } from "./targets.js";
@@ -153,7 +154,7 @@ function generatedCommandSkillContent(params: {
return [
"---",
`name: ${params.skillName}`,
- `description: ${JSON.stringify(description.slice(0, 180))}`,
+ `description: ${JSON.stringify(truncateUtf16Safe(description, 180))}`,
"disable-model-invocation: true",
"---",
"",