Files
elizaos--eliza/plugins/plugin-shell/utils/ptyKeys.ts
T
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

324 lines
7.0 KiB
TypeScript

/**
* Encodes keyboard input into the escape sequences a PTY expects — bracketed
* paste, control keys, and named key sequences — and strips terminal DSR
* (device-status-report) requests from output. Used when driving interactive
* PTY sessions.
*/
const ESC = "\x1b";
const CR = "\r";
const TAB = "\t";
const BACKSPACE = "\x7f";
export const BRACKETED_PASTE_START = `${ESC}[200~`;
export const BRACKETED_PASTE_END = `${ESC}[201~`;
type Modifiers = {
ctrl: boolean;
alt: boolean;
shift: boolean;
};
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const namedKeyMap = new Map<string, string>([
["enter", CR],
["return", CR],
["tab", TAB],
["escape", ESC],
["esc", ESC],
["space", " "],
["bspace", BACKSPACE],
["backspace", BACKSPACE],
["up", `${ESC}[A`],
["down", `${ESC}[B`],
["right", `${ESC}[C`],
["left", `${ESC}[D`],
["home", `${ESC}[1~`],
["end", `${ESC}[4~`],
["pageup", `${ESC}[5~`],
["pgup", `${ESC}[5~`],
["ppage", `${ESC}[5~`],
["pagedown", `${ESC}[6~`],
["pgdn", `${ESC}[6~`],
["npage", `${ESC}[6~`],
["insert", `${ESC}[2~`],
["ic", `${ESC}[2~`],
["delete", `${ESC}[3~`],
["del", `${ESC}[3~`],
["dc", `${ESC}[3~`],
["btab", `${ESC}[Z`],
["f1", `${ESC}OP`],
["f2", `${ESC}OQ`],
["f3", `${ESC}OR`],
["f4", `${ESC}OS`],
["f5", `${ESC}[15~`],
["f6", `${ESC}[17~`],
["f7", `${ESC}[18~`],
["f8", `${ESC}[19~`],
["f9", `${ESC}[20~`],
["f10", `${ESC}[21~`],
["f11", `${ESC}[23~`],
["f12", `${ESC}[24~`],
["kp/", `${ESC}Oo`],
["kp*", `${ESC}Oj`],
["kp-", `${ESC}Om`],
["kp+", `${ESC}Ok`],
["kp7", `${ESC}Ow`],
["kp8", `${ESC}Ox`],
["kp9", `${ESC}Oy`],
["kp4", `${ESC}Ot`],
["kp5", `${ESC}Ou`],
["kp6", `${ESC}Ov`],
["kp1", `${ESC}Oq`],
["kp2", `${ESC}Or`],
["kp3", `${ESC}Os`],
["kp0", `${ESC}Op`],
["kp.", `${ESC}On`],
["kpenter", `${ESC}OM`],
]);
const modifiableNamedKeys = new Set([
"up",
"down",
"left",
"right",
"home",
"end",
"pageup",
"pgup",
"ppage",
"pagedown",
"pgdn",
"npage",
"insert",
"ic",
"delete",
"del",
"dc",
]);
export type KeyEncodingRequest = {
keys?: string[];
hex?: string[];
literal?: string;
};
export type KeyEncodingResult = {
data: string;
warnings: string[];
};
export function encodeKeySequence(request: KeyEncodingRequest): KeyEncodingResult {
const warnings: string[] = [];
let data = "";
if (request.literal) {
data += request.literal;
}
if (request.hex?.length) {
for (const raw of request.hex) {
const byte = parseHexByte(raw);
if (byte === null) {
warnings.push(`Invalid hex byte: ${raw}`);
continue;
}
data += String.fromCharCode(byte);
}
}
if (request.keys?.length) {
for (const token of request.keys) {
data += encodeKeyToken(token, warnings);
}
}
return { data, warnings };
}
export function encodePaste(text: string, bracketed = true): string {
if (!bracketed) {
return text;
}
return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
}
function encodeKeyToken(raw: string, warnings: string[]): string {
const token = raw.trim();
if (!token) {
return "";
}
if (token.length === 2 && token.startsWith("^")) {
const ctrl = toCtrlChar(token[1]);
if (ctrl) {
return ctrl;
}
}
const parsed = parseModifiers(token);
const base = parsed.base;
const baseLower = base.toLowerCase();
if (baseLower === "tab" && parsed.mods.shift) {
return `${ESC}[Z`;
}
const baseSeq = namedKeyMap.get(baseLower);
if (baseSeq) {
let seq = baseSeq;
if (modifiableNamedKeys.has(baseLower) && hasAnyModifier(parsed.mods)) {
const mod = xtermModifier(parsed.mods);
if (mod > 1) {
const modified = applyXtermModifier(seq, mod);
if (modified) {
seq = modified;
return seq;
}
}
}
if (parsed.mods.alt) {
return `${ESC}${seq}`;
}
return seq;
}
if (base.length === 1) {
return applyCharModifiers(base, parsed.mods);
}
if (parsed.hasModifiers) {
warnings.push(`Unknown key "${base}" for modifiers; sending literal.`);
}
return base;
}
function parseModifiers(token: string): {
mods: Modifiers;
base: string;
hasModifiers: boolean;
} {
const mods: Modifiers = { ctrl: false, alt: false, shift: false };
let rest = token;
let sawModifiers = false;
while (rest.length > 2 && rest[1] === "-") {
const mod = rest[0].toLowerCase();
if (mod === "c") {
mods.ctrl = true;
} else if (mod === "m") {
mods.alt = true;
} else if (mod === "s") {
mods.shift = true;
} else {
break;
}
sawModifiers = true;
rest = rest.slice(2);
}
return { mods, base: rest, hasModifiers: sawModifiers };
}
function applyCharModifiers(char: string, mods: Modifiers): string {
let value = char;
if (mods.shift && value.length === 1 && /[a-z]/.test(value)) {
value = value.toUpperCase();
}
if (mods.ctrl) {
const ctrl = toCtrlChar(value);
if (ctrl) {
value = ctrl;
}
}
if (mods.alt) {
value = `${ESC}${value}`;
}
return value;
}
function toCtrlChar(char: string): string | null {
if (char.length !== 1) {
return null;
}
if (char === "?") {
return "\x7f";
}
const code = char.toUpperCase().charCodeAt(0);
if (code >= 64 && code <= 95) {
return String.fromCharCode(code & 0x1f);
}
return null;
}
function xtermModifier(mods: Modifiers): number {
let mod = 1;
if (mods.shift) {
mod += 1;
}
if (mods.alt) {
mod += 2;
}
if (mods.ctrl) {
mod += 4;
}
return mod;
}
function applyXtermModifier(sequence: string, modifier: number): string | null {
const escPattern = escapeRegExp(ESC);
const csiNumber = new RegExp(`^${escPattern}\\[(\\d+)([~A-Z])$`);
const csiArrow = new RegExp(`^${escPattern}\\[(A|B|C|D|H|F)$`);
const numberMatch = sequence.match(csiNumber);
if (numberMatch) {
return `${ESC}[${numberMatch[1]};${modifier}${numberMatch[2]}`;
}
const arrowMatch = sequence.match(csiArrow);
if (arrowMatch) {
return `${ESC}[1;${modifier}${arrowMatch[1]}`;
}
return null;
}
function hasAnyModifier(mods: Modifiers): boolean {
return mods.ctrl || mods.alt || mods.shift;
}
function parseHexByte(raw: string): number | null {
const trimmed = raw.trim().toLowerCase();
const normalized = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
if (!/^[0-9a-f]{1,2}$/.test(normalized)) {
return null;
}
const value = Number.parseInt(normalized, 16);
if (Number.isNaN(value) || value < 0 || value > 0xff) {
return null;
}
return value;
}
// DSR (Device Status Report) handling
const DSR_PATTERN = new RegExp(`${ESC}\\[\\??6n`, "g");
export function stripDsrRequests(input: string): {
cleaned: string;
requests: number;
} {
let requests = 0;
const cleaned = input.replace(DSR_PATTERN, () => {
requests += 1;
return "";
});
return { cleaned, requests };
}
export function buildCursorPositionResponse(row = 1, col = 1): string {
return `\x1b[${row};${col}R`;
}