feat: Complete zCode CLI X with Telegram bot integration

- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation

🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
This commit is contained in:
admin
2026-05-05 09:01:26 +00:00
Unverified
parent 4a7035dd92
commit 875c7f9b91
24688 changed files with 3224957 additions and 221 deletions

View File

@@ -0,0 +1 @@
/home/uroma2/zcode-cli-x/~/.npm-cache/@alcalzone/ansi-tokenize@0.3.0@@@1

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2026 AlCalzone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,278 @@
# @alcalzone/ansi-tokenize
> Efficiently modify strings containing [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles)
If you find yourself modifying styled strings repeatedly, alternatives like [`slice-ansi`](https://github.com/chalk/slice-ansi/) may end up doing a lot of unnecessary work by re-parsing the string each time. This module provides a way to parse the string into an array of tokens (characters or ANSI codes), which can then be modified and re-serialized into a styled string.
## Install
```
$ npm install @alcalzone/ansi-tokenize
```
## Usage
### Tokenize a string
```js
import { tokenize } from "@alcalzone/ansi-tokenize";
// red "foo", followed by unstyled "bar"
const str = "\x1B[31mfoo\x1B[39mbar";
const tokens = tokenize(str);
// tokens will now look like this:
[
{
type: "ansi",
code: "\x1B[31m",
endCode: "\x1B[39m",
},
{
type: "char",
value: "f",
fullWidth: false,
},
{
type: "char",
value: "o",
fullWidth: false,
},
{
type: "char",
value: "o",
fullWidth: false,
},
{
type: "ansi",
code: "\x1B[39m",
endCode: "\x1B[39m",
},
{
type: "char",
value: "b",
fullWidth: false,
},
{
type: "char",
value: "a",
fullWidth: false,
},
{
type: "char",
value: "r",
fullWidth: false,
},
];
```
Each token is either a character
```ts
export interface Char {
type: "char";
value: string;
fullWidth: boolean;
}
```
where
- `value` is the string representation of the character
- `fullWidth` is `true` if the character is full width (takes up 2 characters in monospace, like CJK characters)
or an ANSI code
```ts
export interface AnsiCode {
type: "ansi";
code: string;
endCode: string;
}
```
where
- `code` is the ANSI code that starts the style
- and `endCode` is the corresponding ANSI code that ends the style.
An `AnsiCode` can also be an end code, in which case `code` and `endCode` will be the same.
### Convert an array of tokens into an array of "styled" chars
This representation is a 1:1 mapping of the original string, but not very useful for modifying the string. The `styledCharsFromTokens` function converts a token array to an array of characters, where each character has an all currently active styles associated with it:
```ts
export interface StyledChar {
type: "char";
value: string;
fullWidth: boolean;
styles: AnsiCode[];
}
```
Using the above example:
```js
import { tokenize, styledCharsFromTokens } from "@alcalzone/ansi-tokenize";
// red "foo", followed by unstyled "bar"
const str = "\x1B[31mfoo\x1B[39mbar";
const tokens = tokenize(str);
const styledChars = styledCharsFromTokens(tokens);
// styledChars will contain the following:
[
{
type: "char",
value: "f",
fullWidth: false,
styles: [
{
type: "ansi",
code: "\x1B[31m",
endCode: "\x1B[39m",
},
],
},
{
type: "char",
value: "o",
fullWidth: false,
styles: [
{
type: "ansi",
code: "\x1B[31m",
endCode: "\x1B[39m",
},
],
},
{
type: "char",
value: "o",
fullWidth: false,
styles: [
{
type: "ansi",
code: "\x1B[31m",
endCode: "\x1B[39m",
},
],
},
{
type: "char",
value: "b",
fullWidth: false,
styles: [],
},
{
type: "char",
value: "a",
fullWidth: false,
styles: [],
},
{
type: "char",
value: "r",
fullWidth: false,
styles: [],
},
];
```
### Modify an array of styled characters
For modification simply edit the items in the array as necessary, as long as the following rules are followed:
1. The `code` and `endCode` properties must match. You can use the `ansi-styles` module to do this.
2. The `fullWidth` property must be correct. You can use the `is-fullwidth-code-point` module to do this, or if working with multiple strings, turn those into styled char arrays first.
E.g. to make the first `o` blue and bold:
```js
import ansiStyles from "ansi-styles";
// ... include the above code
styledChars[1].styles = [
{
type: "ansi",
code: ansiStyles.blue.open,
endCode: ansiStyles.blue.close,
},
{
type: "ansi",
code: ansiStyles.bold.open,
endCode: ansiStyles.bold.close,
},
];
```
### Serialize a styled character array back to a string
The `styledCharsToString` function converts a styled character array back to a string:
```js
import { styledCharsToString } from "@alcalzone/ansi-tokenize";
// ... include the above code
const strOut = styledCharsToString(styledChars);
// str will now be '\x1B[31mf\x1B[34m\x1B[1mo\x1B[22m\x1B[31mo\x1B[39mbar'
```
This automatically figures out the least amount of escape codes necessary to achieve the desired result, as long as the `styles` arrays contain no unnecessary styles, e.g. blue + red foreground.
## Changelog
<!--
Placeholder for next release:
### __WORK IN PROGRESS__
-->
### 0.3.0 (2026-02-20)
- Fix: preserve non-hyperlink OSC sequences when tokenizing (#54)
- Fix: support ST-terminated OSC hyperlinks (#53)
### 0.2.5 (2026-02-11)
- Fix: preserve grapheme clusters when tokenizing (#51)
### 0.2.4 (2026-01-29)
- Fix: Support hyperlinks with parameters (#45)
### 0.2.3 (2026-01-02)
- Fix: Regression when rendering adjacent `dim` and `bold` styles (#43, #44)
### 0.2.2 (2025-10-22)
- Fix: Support compound SGR sequences with multiple attributes (#39, #40)
### 0.2.1 (2025-10-20)
- Fix: Prevent `dim` and `bold` modifiers from canceling each other (#37)
### 0.2.0 (2025-04-24)
- Breaking: Require Node.js 18+
- Fix: Detect emojis as being full width
### 0.1.3 (2023-09-07)
- Fix: Support links
### 0.1.2 (2023-08-07)
- Fix: Reduce minimum Node.js version to `14.13.1`
### 0.1.1 (2023-04-05)
- Fix: Active styles are now correctly reset at the end of the string
### 0.1.0 (2023-03-20)
Initial release

View File

@@ -0,0 +1,7 @@
import type { AnsiCode } from "./tokenize.js";
export declare const endCodesSet: Set<string>;
export declare function getLinkStartCode(url: string, params?: Record<string, string>): string;
export declare function getEndCode(code: string): string;
export declare function ansiCodesToString(codes: AnsiCode[]): string;
/** Check if a code is an intensity code (bold or dim) - these share endCode 22m but can coexist */
export declare function isIntensityCode(code: AnsiCode): boolean;

View File

@@ -0,0 +1,57 @@
import ansiStyles from "ansi-styles";
import { linkCodePrefix, linkCodeSuffix, linkEndCode, linkEndCodeC1ST, linkEndCodeST, } from "./consts.js";
export const endCodesSet = new Set();
const endCodesMap = new Map();
for (const [start, end] of ansiStyles.codes) {
endCodesSet.add(ansiStyles.color.ansi(end));
endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
}
export function getLinkStartCode(url, params) {
const paramsStr = params
? Object.entries(params)
.map(([k, v]) => `${k}=${v}`)
.join(":")
: "";
return `${linkCodePrefix}${paramsStr};${url}${linkCodeSuffix}`;
}
export function getEndCode(code) {
if (endCodesSet.has(code))
return code;
if (endCodesMap.has(code))
return endCodesMap.get(code);
// We have a few special cases to handle here:
// Links:
if (code.startsWith(linkCodePrefix)) {
if (code.endsWith("\x1B\\"))
return linkEndCodeST;
if (code.endsWith("\x9C"))
return linkEndCodeC1ST;
return linkEndCode; // BEL (\x07)
}
code = code.slice(2);
// 8-bit/24-bit colors:
if (code.startsWith("38")) {
return ansiStyles.color.close;
}
else if (code.startsWith("48")) {
return ansiStyles.bgColor.close;
}
// Otherwise find the reset code in the ansi-styles map
const ret = ansiStyles.codes.get(parseInt(code, 10));
if (ret) {
return ansiStyles.color.ansi(ret);
}
else {
return ansiStyles.reset.open;
}
}
export function ansiCodesToString(codes) {
// Deduplicate ANSI code strings before joining
const deduplicated = new Set(codes.map((code) => code.code));
return [...deduplicated].join("");
}
/** Check if a code is an intensity code (bold or dim) - these share endCode 22m but can coexist */
export function isIntensityCode(code) {
return code.code === ansiStyles.bold.open || code.code === ansiStyles.dim.open;
}
//# sourceMappingURL=ansiCodes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ansiCodes.js","sourceRoot":"","sources":["../src/ansiCodes.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,17 @@
export declare const CC_BEL: number;
export declare const CC_ESC: number;
export declare const CC_BACKSLASH: number;
export declare const CC_CSI: number;
export declare const CC_OSC: number;
export declare const CC_C1_ST: number;
export declare const CC_0: number;
export declare const CC_9: number;
export declare const CC_SEMI: number;
export declare const CC_M: number;
export declare const ESCAPES: Set<number>;
export declare const linkCodePrefix: string;
export declare const linkCodePrefixCharCodes: number[];
export declare const linkCodeSuffix = "\u0007";
export declare const linkEndCode: string;
export declare const linkEndCodeST: string;
export declare const linkEndCodeC1ST: string;

View File

@@ -0,0 +1,28 @@
// Named ANSI control characters
const BEL = "\x07";
const ESC = "\x1b";
const BACKSLASH = "\\";
const CSI = "[";
const OSC = "]";
const C1_ST = "\x9c";
// Char codes (derived from named characters)
export const CC_BEL = BEL.charCodeAt(0);
export const CC_ESC = ESC.charCodeAt(0);
export const CC_BACKSLASH = BACKSLASH.charCodeAt(0);
export const CC_CSI = CSI.charCodeAt(0);
export const CC_OSC = OSC.charCodeAt(0);
export const CC_C1_ST = C1_ST.charCodeAt(0);
export const CC_0 = "0".charCodeAt(0);
export const CC_9 = "9".charCodeAt(0);
export const CC_SEMI = ";".charCodeAt(0);
export const CC_M = "m".charCodeAt(0);
// Escape code points
export const ESCAPES = new Set([CC_ESC, 0x9b]); // \x1b and \x9b
// OSC 8 hyperlink constants
export const linkCodePrefix = `${ESC}${OSC}8;`;
export const linkCodePrefixCharCodes = linkCodePrefix.split("").map((char) => char.charCodeAt(0));
export const linkCodeSuffix = BEL;
export const linkEndCode = `${ESC}${OSC}8;;${BEL}`;
export const linkEndCodeST = `${ESC}${OSC}8;;${ESC}${BACKSLASH}`;
export const linkEndCodeC1ST = `${ESC}${OSC}8;;${C1_ST}`;
//# sourceMappingURL=consts.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,MAAM,GAAG,GAAG,MAAM,CAAC;AACnB,MAAM,GAAG,GAAG,MAAM,CAAC;AACnB,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,GAAG,GAAG,GAAG,CAAC;AAChB,MAAM,GAAG,GAAG,GAAG,CAAC;AAChB,MAAM,KAAK,GAAG,MAAM,CAAC;AAErB,6CAA6C;AAC7C,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,YAAY,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACpD,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC5C,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACzC,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAEtC,qBAAqB;AACrB,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,gBAAgB;AAEhE,4BAA4B;AAC5B,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAC/C,MAAM,CAAC,MAAM,uBAAuB,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAClG,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,EAAE,CAAC;AACnD,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,KAAK,EAAE,CAAC"}

View File

@@ -0,0 +1,6 @@
import type { AnsiCode } from "./tokenize.js";
/**
* Returns the minimum amount of ANSI codes necessary to get from the compound style `from` to `to`.
* Both `from` and `to` are expected to be reduced.
*/
export declare function diffAnsiCodes(from: AnsiCode[], to: AnsiCode[]): AnsiCode[];

View File

@@ -0,0 +1,26 @@
import { isIntensityCode } from "./ansiCodes.js";
import { undoAnsiCodes } from "./undo.js";
/**
* Returns the minimum amount of ANSI codes necessary to get from the compound style `from` to `to`.
* Both `from` and `to` are expected to be reduced.
*/
export function diffAnsiCodes(from, to) {
const endCodesInTo = new Set(to.map((code) => code.endCode));
const startCodesInTo = new Set(to.map((code) => code.code));
const startCodesInFrom = new Set(from.map((code) => code.code));
return [
// Ignore all styles in `from` that are not overwritten or removed by `to`
// Disable all styles in `from` that are removed in `to`
...undoAnsiCodes(from.filter((code) => {
// Special case: Intensity codes (1m, 2m) can coexist (both end with 22m).
// We have to check the start codes for those, otherwise we might miss a reset.
if (isIntensityCode(code)) {
return !startCodesInTo.has(code.code);
}
return !endCodesInTo.has(code.endCode);
})),
// Add all styles in `to` that don't exist in `from`
...to.filter((code) => !startCodesInFrom.has(code.code)),
];
}
//# sourceMappingURL=diff.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAgB,EAAE,EAAc;IAC7D,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7D,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEhE,OAAO;QACN,0EAA0E;QAC1E,wDAAwD;QACxD,GAAG,aAAa,CACf,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YACpB,0EAA0E;YAC1E,+EAA+E;YAC/E,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE;gBAC1B,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aACtC;YACD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC,CAAC,CACF;QACD,oDAAoD;QACpD,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KACxD,CAAC;AACH,CAAC"}

View File

@@ -0,0 +1,6 @@
export { ansiCodesToString } from "./ansiCodes.js";
export { diffAnsiCodes } from "./diff.js";
export { reduceAnsiCodes, reduceAnsiCodesIncremental } from "./reduce.js";
export * from "./styledChars.js";
export * from "./tokenize.js";
export { undoAnsiCodes } from "./undo.js";

View File

@@ -0,0 +1,7 @@
export { ansiCodesToString } from "./ansiCodes.js";
export { diffAnsiCodes } from "./diff.js";
export { reduceAnsiCodes, reduceAnsiCodesIncremental } from "./reduce.js";
export * from "./styledChars.js";
export * from "./tokenize.js";
export { undoAnsiCodes } from "./undo.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAC1E,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC"}

View File

@@ -0,0 +1,5 @@
import type { AnsiCode } from "./tokenize.js";
/** Reduces the given array of ANSI codes to the minimum necessary to render with the same style */
export declare function reduceAnsiCodes(codes: AnsiCode[]): AnsiCode[];
/** Like {@link reduceAnsiCodes}, but assumes that `codes` is already reduced. Further reductions are only done for the items in `newCodes`. */
export declare function reduceAnsiCodesIncremental(codes: AnsiCode[], newCodes: AnsiCode[]): AnsiCode[];

View File

@@ -0,0 +1,37 @@
import ansiStyles from "ansi-styles";
import { endCodesSet, isIntensityCode } from "./ansiCodes.js";
/** Reduces the given array of ANSI codes to the minimum necessary to render with the same style */
export function reduceAnsiCodes(codes) {
return reduceAnsiCodesIncremental([], codes);
}
/** Like {@link reduceAnsiCodes}, but assumes that `codes` is already reduced. Further reductions are only done for the items in `newCodes`. */
export function reduceAnsiCodesIncremental(codes, newCodes) {
let ret = [...codes];
for (const code of newCodes) {
if (code.code === ansiStyles.reset.open) {
// Reset code, disable all codes
ret = [];
}
else if (endCodesSet.has(code.code)) {
// This is an end code, disable all matching start codes
ret = ret.filter((retCode) => retCode.endCode !== code.code);
}
else {
// This is a start code. Remove codes it "overrides", then add it.
// If a new code has the same endCode, it "overrides" existing ones.
// Special case: Intensity codes (1m, 2m) can coexist (both end with 22m).
// We only add those if the exact same code is not already present.
if (isIntensityCode(code)) {
if (!ret.find((retCode) => retCode.code === code.code && retCode.endCode === code.endCode)) {
ret.push(code);
}
}
else {
ret = ret.filter((retCode) => retCode.endCode !== code.endCode);
ret.push(code);
}
}
}
return ret;
}
//# sourceMappingURL=reduce.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"reduce.js","sourceRoot":"","sources":["../src/reduce.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG9D,mGAAmG;AACnG,MAAM,UAAU,eAAe,CAAC,KAAiB;IAChD,OAAO,0BAA0B,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,+IAA+I;AAC/I,MAAM,UAAU,0BAA0B,CAAC,KAAiB,EAAE,QAAoB;IACjF,IAAI,GAAG,GAAe,CAAC,GAAG,KAAK,CAAC,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE;YACxC,gCAAgC;YAChC,GAAG,GAAG,EAAE,CAAC;SACT;aAAM,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YACtC,wDAAwD;YACxD,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;SAC7D;aAAM;YACN,kEAAkE;YAClE,oEAAoE;YACpE,0EAA0E;YAC1E,mEAAmE;YACnE,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE;gBAC1B,IACC,CAAC,GAAG,CAAC,IAAI,CACR,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAC3E,EACA;oBACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;iBACf;aACD;iBAAM;gBACN,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC;gBAChE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aACf;SACD;KACD;IACD,OAAO,GAAG,CAAC;AACZ,CAAC"}

View File

@@ -0,0 +1,6 @@
import type { AnsiCode, Char, Token } from "./tokenize.js";
export interface StyledChar extends Char {
styles: AnsiCode[];
}
export declare function styledCharsFromTokens(tokens: Token[]): StyledChar[];
export declare function styledCharsToString(chars: StyledChar[]): string;

View File

@@ -0,0 +1,38 @@
import { ansiCodesToString } from "./ansiCodes.js";
import { diffAnsiCodes } from "./diff.js";
import { reduceAnsiCodesIncremental } from "./reduce.js";
export function styledCharsFromTokens(tokens) {
let codes = [];
const ret = [];
for (const token of tokens) {
if (token.type === "ansi") {
codes = reduceAnsiCodesIncremental(codes, [token]);
}
else if (token.type === "char") {
ret.push({
...token,
styles: [...codes],
});
}
}
return ret;
}
export function styledCharsToString(chars) {
let ret = "";
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
if (i === 0) {
ret += ansiCodesToString(char.styles);
}
else {
ret += ansiCodesToString(diffAnsiCodes(chars[i - 1].styles, char.styles));
}
ret += char.value;
// reset active styles at the end of the string
if (i === chars.length - 1) {
ret += ansiCodesToString(diffAnsiCodes(char.styles, []));
}
}
return ret;
}
//# sourceMappingURL=styledChars.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"styledChars.js","sourceRoot":"","sources":["../src/styledChars.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAOzD,MAAM,UAAU,qBAAqB,CAAC,MAAe;IACpD,IAAI,KAAK,GAAe,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE;YAC1B,KAAK,GAAG,0BAA0B,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;SACnD;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE;YACjC,GAAG,CAAC,IAAI,CAAC;gBACR,GAAG,KAAK;gBACR,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC;aAClB,CAAC,CAAC;SACH;KACD;IACD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAmB;IACtD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,EAAE;YACZ,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;SACtC;aAAM;YACN,GAAG,IAAI,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;SAC1E;QACD,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC;QAClB,+CAA+C;QAC/C,IAAI,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE;YAC3B,GAAG,IAAI,iBAAiB,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;SACzD;KACD;IACD,OAAO,GAAG,CAAC;AACZ,CAAC"}

View File

@@ -0,0 +1,16 @@
export interface AnsiCode {
type: "ansi";
code: string;
endCode: string;
}
export interface ControlCode {
type: "control";
code: string;
}
export interface Char {
type: "char";
value: string;
fullWidth: boolean;
}
export type Token = AnsiCode | ControlCode | Char;
export declare function tokenize(str: string, endChar?: number): Token[];

View File

@@ -0,0 +1,194 @@
import isFullwidthCodePoint from "is-fullwidth-code-point";
import { getEndCode } from "./ansiCodes.js";
import { CC_0, CC_9, CC_BEL, CC_BACKSLASH, CC_C1_ST, CC_ESC, CC_M, CC_CSI, CC_OSC, CC_SEMI, ESCAPES, linkCodePrefix, linkCodePrefixCharCodes, } from "./consts.js";
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function isFullwidthGrapheme(grapheme, baseCodePoint) {
if (isFullwidthCodePoint(baseCodePoint))
return true;
// Variation Selector 16 forces emoji presentation (2 columns wide)
if (grapheme.includes("\uFE0F"))
return true;
// Regional indicator pairs form flag emoji (2 columns wide)
if (baseCodePoint >= 0x1f1e6 && baseCodePoint <= 0x1f1ff)
return true;
return false;
}
// HOT PATH: Use only basic string/char code operations for maximum performance
function parseLinkCode(string, offset) {
string = string.slice(offset);
for (let index = 1; index < linkCodePrefixCharCodes.length; index++) {
if (string.charCodeAt(index) !== linkCodePrefixCharCodes[index]) {
return undefined;
}
}
// Find the semicolon that ends params
const paramsEndIndex = string.indexOf(";", linkCodePrefix.length);
if (paramsEndIndex === -1)
return undefined;
// This is a link code (with or without the URL part). Find the end of it.
const endIndex = findOSCTerminatorIndex(string, paramsEndIndex + 1);
if (endIndex === -1)
return undefined;
return string.slice(0, endIndex + 1);
}
// HOT PATH: Generic fallback for non-link OSC sequences (window title, notifications, etc.)
function parseOSCSequence(string, offset) {
string = string.slice(offset);
// Find the OSC terminator (starting after "ESC ]")
const endIndex = findOSCTerminatorIndex(string, 2);
if (endIndex === -1)
return undefined;
return string.slice(0, endIndex + 1);
}
/**
* Finds the index of the last character of the first OSC terminator at or after startIndex.
* Recognizes BEL (\x07), C1 ST (\x9C), and ESC+backslash (\x1B\x5C).
* Returns -1 if no terminator is found.
*/
function findOSCTerminatorIndex(string, startIndex) {
for (let i = startIndex; i < string.length; i++) {
const ch = string.charCodeAt(i);
if (ch === CC_BEL)
return i;
if (ch === CC_C1_ST)
return i;
if (ch === CC_ESC && i + 1 < string.length && string.charCodeAt(i + 1) === CC_BACKSLASH) {
return i + 1;
}
}
return -1;
}
/**
* Scans through the given string and finds the index of the last character of an SGR sequence
* like `\x1B[38;2;123;123;123m`. This assumes that the string has been checked to start with `\x1B[`.
* Returns -1 if no valid SGR sequence is found.
*/
function findSGRSequenceEndIndex(str) {
for (let index = 2; index < str.length; index++) {
const charCode = str.charCodeAt(index);
// m marks the end of the SGR sequence
if (charCode === CC_M)
return index;
// Digits and semicolons are valid
if (charCode === CC_SEMI)
continue;
if (charCode >= CC_0 && charCode <= CC_9)
continue;
// Everything else is invalid
break;
}
return -1;
}
// HOT PATH: Use only basic string/char code operations for maximum performance
function parseSGRSequence(string, offset) {
string = string.slice(offset);
const endIndex = findSGRSequenceEndIndex(string);
if (endIndex === -1)
return;
return string.slice(0, endIndex + 1);
}
/**
* Splits compound SGR sequences like `\x1B[1;3;31m` into individual components
*/
function splitCompoundSGRSequences(code) {
if (!code.includes(";")) {
// Not a compound code
return [code];
}
const codeParts = code
// Strip off the escape sequences \x1B[ and m
.slice(2, -1)
.split(";");
const ret = [];
for (let i = 0; i < codeParts.length; i++) {
const rawCode = codeParts[i];
// Keep 8-bit and 24-bit color codes (containing multiple ";") together
if (rawCode === "38" || rawCode === "48") {
if (i + 2 < codeParts.length && codeParts[i + 1] === "5") {
// 8-bit color, followed by another number
ret.push(codeParts.slice(i, i + 3).join(";"));
i += 2;
continue;
}
else if (i + 4 < codeParts.length && codeParts[i + 1] === "2") {
// 24-bit color, followed by three numbers
ret.push(codeParts.slice(i, i + 5).join(";"));
i += 4;
continue;
}
}
// Not a (valid) 8/24-bit color code, push as is
ret.push(rawCode);
}
return ret.map((part) => `\x1b[${part}m`);
}
export function tokenize(str, endChar = Number.POSITIVE_INFINITY) {
const ret = [];
let visible = 0;
let codeEndIndex = 0;
for (const { segment, index } of segmenter.segment(str)) {
// Skip segments consumed as part of an ANSI sequence
if (index < codeEndIndex)
continue;
const codePoint = segment.codePointAt(0);
if (ESCAPES.has(codePoint)) {
let code;
// Peek the next code point to determine the type of ANSI sequence
const nextCodePoint = str.codePointAt(index + 1);
if (nextCodePoint === CC_OSC) {
// ] = operating system commands
code = parseLinkCode(str, index);
if (code) {
// OSC 8 hyperlinks are paired codes with an endCode
ret.push({
type: "ansi",
code: code,
endCode: getEndCode(code),
});
}
else {
// Other OSC sequences (window title, etc.) are self-contained
// control codes with no endCode.
code = parseOSCSequence(str, index);
if (code) {
ret.push({
type: "control",
code: code,
});
}
}
}
else if (nextCodePoint === CC_CSI) {
// [ = control sequence introducer, like SGR sequences [...m
code = parseSGRSequence(str, index);
if (code) {
// Split compound codes into individual tokens
const codes = splitCompoundSGRSequences(code);
for (const individualCode of codes) {
ret.push({
type: "ansi",
code: individualCode,
endCode: getEndCode(individualCode),
});
}
}
}
if (code) {
codeEndIndex = index + code.length;
continue;
}
}
const fullWidth = isFullwidthGrapheme(segment, codePoint);
ret.push({
type: "char",
value: segment,
fullWidth,
});
visible += fullWidth ? 2 : 1;
if (visible >= endChar) {
break;
}
}
return ret;
}
//# sourceMappingURL=tokenize.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import type { AnsiCode } from "./tokenize.js";
/** Returns the combination of ANSI codes needed to undo the given ANSI codes */
export declare function undoAnsiCodes(codes: AnsiCode[]): AnsiCode[];

View File

@@ -0,0 +1,11 @@
import { reduceAnsiCodes } from "./reduce.js";
/** Returns the combination of ANSI codes needed to undo the given ANSI codes */
export function undoAnsiCodes(codes) {
return reduceAnsiCodes(codes)
.reverse()
.map((code) => ({
...code,
code: code.endCode,
}));
}
//# sourceMappingURL=undo.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"undo.js","sourceRoot":"","sources":["../src/undo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC9C,OAAO,eAAe,CAAC,KAAK,CAAC;SAC3B,OAAO,EAAE;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACf,GAAG,IAAI;QACP,IAAI,EAAE,IAAI,CAAC,OAAO;KAClB,CAAC,CAAC,CAAC;AACN,CAAC"}

View File

@@ -0,0 +1,55 @@
{
"name": "@alcalzone/ansi-tokenize",
"version": "0.3.0",
"description": "Efficiently modify strings containing ANSI escape codes",
"publishConfig": {
"access": "public"
},
"author": {
"name": "AlCalzone",
"email": "d.griesel@gmx.net"
},
"license": "MIT",
"type": "module",
"module": "build/index.js",
"types": "build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"import": "./build/index.js"
},
"./package.json": "./package.json"
},
"files": [
"build"
],
"engines": {
"node": ">=18"
},
"devDependencies": {
"@alcalzone/release-script": "~4.0.0",
"@alcalzone/release-script-plugin-license": "~4.0.0",
"@tsconfig/node18": "^18.2.4",
"@types/node": "^18.19.130",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.4",
"typescript": "^5.0.2",
"vitest": "^3.2.4"
},
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"scripts": {
"prepare": "tsc -p tsconfig.build.json",
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"lint": "eslint .",
"release": "release-script"
},
"packageManager": "yarn@4.10.3"
}