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", "---", "",