@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
171 lines (170 loc) • 9.46 kB
JavaScript
/**
* Phren character ASCII/Unicode art for CLI presence.
*
* Based on the pixel art: purple 8-bit brain with diamond eyes,
* smile, little legs, and cyan sparkle.
*/
/**
* Phren truecolor art (24px wide, generated from phren-transparent.png).
* Uses half-block ▀ with RGB foreground+background for pixel-faithful rendering.
* Requires truecolor terminal (most modern terminals support it).
*/
export const PHREN_ART = [
" ",
" ",
" \x1b[38;2;40;211;242m▄\x1b[0m \x1b[38;2;27;210;241m▄\x1b[0m ",
" \x1b[38;2;38;39;142m▄\x1b[0m\x1b[38;2;153;140;248m▄\x1b[0m\x1b[38;2;40;41;142m\x1b[48;2;152;146;247m▀\x1b[0m\x1b[38;2;41;43;144m\x1b[48;2;93;67;243m▀\x1b[0m\x1b[38;2;157;147;250m▄\x1b[0m\x1b[38;2;43;44;147m\x1b[48;2;156;146;249m▀\x1b[0m\x1b[38;2;41;43;144m\x1b[48;2;145;147;247m▀\x1b[0m\x1b[38;2;155;146;248m▄\x1b[0m\x1b[38;2;41;40;141m▄\x1b[0m ",
" \x1b[38;2;39;39;132m▄\x1b[0m\x1b[38;2;150;132;250m\x1b[48;2;151;133;250m▀\x1b[0m\x1b[38;2;154;143;250m\x1b[48;2;148;129;251m▀\x1b[0m\x1b[38;2;104;75;249m\x1b[48;2;156;145;248m▀\x1b[0m\x1b[38;2;156;142;251m\x1b[48;2;92;68;236m▀\x1b[0m\x1b[38;2;156;149;248m\x1b[48;2;85;70;220m▀\x1b[0m\x1b[38;2;157;150;248m\x1b[48;2;157;151;248m▀\x1b[0m\x1b[38;2;151;130;250m\x1b[48;2;86;61;235m▀\x1b[0m\x1b[38;2;149;145;247m\x1b[48;2;105;83;245m▀\x1b[0m\x1b[38;2;155;143;248m\x1b[48;2;191;189;251m▀\x1b[0m\x1b[38;2;41;41;146m\x1b[48;2;153;135;250m▀\x1b[0m\x1b[38;2;71;68;183m▄\x1b[0m ",
" \x1b[38;2;12;31;109m\x1b[48;2;148;132;250m▀\x1b[0m\x1b[38;2;82;67;225m\x1b[48;2;144;126;251m▀\x1b[0m\x1b[38;2;143;122;252m\x1b[48;2;156;143;251m▀\x1b[0m\x1b[38;2;94;67;244m\x1b[48;2;149;132;251m▀\x1b[0m\x1b[38;2;152;144;249m\x1b[48;2;150;132;251m▀\x1b[0m\x1b[38;2;154;143;248m\x1b[48;2;151;133;250m▀\x1b[0m\x1b[38;2;157;153;248m\x1b[48;2;152;134;250m▀\x1b[0m\x1b[38;2;84;61;230m\x1b[48;2;152;139;247m▀\x1b[0m\x1b[38;2;152;139;250m\x1b[48;2;106;93;246m▀\x1b[0m\x1b[38;2;95;71;239m\x1b[48;2;155;141;250m▀\x1b[0m\x1b[38;2;92;68;237m\x1b[48;2;158;141;248m▀\x1b[0m\x1b[38;2;151;139;250m\x1b[48;2;116;101;251m▀\x1b[0m\x1b[38;2;67;61;181m\x1b[48;2;36;41;131m▀\x1b[0m ",
" \x1b[38;2;141;122;250m\x1b[48;2;146;128;248m▀\x1b[0m\x1b[38;2;21;32;101m\x1b[48;2;154;132;250m▀\x1b[0m\x1b[38;2;146;126;251m\x1b[48;2;145;123;251m▀\x1b[0m\x1b[38;2;146;128;250m\x1b[48;2;145;125;250m▀\x1b[0m\x1b[38;2;158;149;250m\x1b[48;2;146;123;248m▀\x1b[0m\x1b[38;2;22;31;104m\x1b[48;2;152;132;248m▀\x1b[0m\x1b[38;2;152;137;250m\x1b[48;2;151;133;251m▀\x1b[0m\x1b[38;2;150;142;249m\x1b[48;2;135;121;250m▀\x1b[0m\x1b[38;2;152;138;250m\x1b[48;2;119;99;247m▀\x1b[0m\x1b[38;2;154;140;251m\x1b[48;2;108;93;249m▀\x1b[0m\x1b[38;2;116;104;252m\x1b[48;2;117;100;251m▀\x1b[0m\x1b[38;2;127;111;251m\x1b[48;2;125;110;250m▀\x1b[0m\x1b[38;2;92;85;242m\x1b[48;2;93;81;242m▀\x1b[0m ",
" \x1b[38;2;10;28;98m▀\x1b[0m\x1b[38;2;147;128;251m\x1b[48;2;77;59;222m▀\x1b[0m\x1b[38;2;145;125;250m\x1b[48;2;100;82;243m▀\x1b[0m\x1b[38;2;48;39;174m\x1b[48;2;136;120;250m▀\x1b[0m\x1b[38;2;146;126;251m\x1b[48;2;102;86;245m▀\x1b[0m\x1b[38;2;146;128;250m\x1b[48;2;103;86;245m▀\x1b[0m\x1b[38;2;111;94;250m\x1b[48;2;116;102;249m▀\x1b[0m\x1b[38;2;122;109;250m\x1b[48;2;114;103;247m▀\x1b[0m\x1b[38;2;120;107;251m\x1b[48;2;86;74;229m▀\x1b[0m\x1b[38;2;121;100;250m\x1b[48;2;106;93;244m▀\x1b[0m\x1b[38;2;92;66;240m\x1b[48;2;36;25;138m▀\x1b[0m\x1b[38;2;117;92;249m\x1b[48;2;83;73;231m▀\x1b[0m\x1b[38;2;7;37;110m▀\x1b[0m ",
" \x1b[38;2;18;22;101m▀\x1b[0m\x1b[38;2;19;24;101m▀\x1b[0m\x1b[38;2;66;51;207m\x1b[48;2;69;51;218m▀\x1b[0m\x1b[38;2;95;83;244m\x1b[48;2;26;24;106m▀\x1b[0m\x1b[38;2;72;59;210m▀\x1b[0m\x1b[38;2;115;96;250m\x1b[48;2;58;46;198m▀\x1b[0m\x1b[38;2;117;104;249m\x1b[48;2;20;31;99m▀\x1b[0m\x1b[38;2;119;104;249m\x1b[48;2;26;29;111m▀\x1b[0m\x1b[38;2;23;21;110m▀\x1b[0m ",
" \x1b[38;2;24;29;112m\x1b[48;2;156;157;248m▀\x1b[0m\x1b[38;2;105;91;248m\x1b[48;2;155;157;248m▀\x1b[0m\x1b[38;2;9;30;102m\x1b[48;2;157;158;248m▀\x1b[0m\x1b[38;2;12;31;104m\x1b[48;2;158;161;248m▀\x1b[0m\x1b[38;2;112;102;250m\x1b[48;2;158;160;248m▀\x1b[0m\x1b[38;2;15;41;120m\x1b[48;2;158;162;248m▀\x1b[0m\x1b[38;2;160;169;250m\x1b[48;2;158;163;247m▀\x1b[0m ",
" ",
" ",
];
// ── Sparkle row: the cyan pixels at row 2 ────────────────────────────────────
const SPARKLE_ROW = 2;
const SPARKLE_CHARS = ["\u2726", "\u2727", "\u2736", " "];
// ── Eye detection: dark navy pixels in row 6 ─────────────────────────────────
const EYE_ROW = 6;
const EYE_R_MAX = 30;
const EYE_G_MAX = 45;
const EYE_B_MAX = 120;
const BLINK_COLOR = "146;130;250";
function flipLine(line) {
const stripped = line.replace(/\x1b\[[^m]*m/g, "");
const leadSpaces = stripped.match(/^( *)/)[1].length;
const trailSpaces = stripped.match(/( *)$/)[1].length;
const pixels = [];
const pixelRegex = /((?:\x1b\[[^m]*m)+)([\u2580\u2584])/g;
let match;
while ((match = pixelRegex.exec(line)) !== null) {
const codes = match[1].replace(/\x1b\[0m/g, "");
if (codes)
pixels.push({ codes, char: match[2] });
}
if (pixels.length === 0)
return line;
const reversed = [...pixels].reverse();
const newLead = " ".repeat(trailSpaces);
const newTrail = " ".repeat(leadSpaces);
let result = newLead;
for (const px of reversed) {
result += px.codes + px.char + "\x1b[0m";
}
result += newTrail;
return result;
}
function generateFlippedArt(art) {
return art.map(flipLine);
}
export const PHREN_ART_RIGHT = generateFlippedArt(PHREN_ART);
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function applyBlinkToLine(line) {
return line.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (full, rStr, gStr, bStr) => {
const r = Number(rStr);
const g = Number(gStr);
const b = Number(bStr);
if (r < EYE_R_MAX && g < EYE_G_MAX && b < EYE_B_MAX) {
return `\x1b[38;2;${BLINK_COLOR}m`;
}
return full;
});
}
function applySparkleToLine(line, frame, active) {
if (!active)
return line;
const sparkleChar = SPARKLE_CHARS[frame % SPARKLE_CHARS.length];
if (sparkleChar === " ") {
return line.replace(/\x1b\[38;2;\d+;2\d\d;2\d\dm[\u2580\u2584]\x1b\[0m/g, " ");
}
return line.replace(/(\x1b\[38;2;\d+;2\d\d;2\d\dm)[\u2580\u2584](\x1b\[0m)/g, `$1${sparkleChar}$2`);
}
export function createPhrenAnimator(options) {
const facing = options?.facing ?? "left";
const baseArt = facing === "right" ? PHREN_ART_RIGHT : PHREN_ART;
const state = {
isBlinking: false,
sparkleFrame: 0,
sparkleActive: false,
};
const timers = [];
function scheduleTimer(fn, ms) {
const t = setTimeout(fn, ms);
timers.push(t);
}
function scheduleBlink() {
const interval = randInt(2000, 8000);
scheduleTimer(() => {
state.isBlinking = true;
scheduleTimer(() => {
state.isBlinking = false;
if (Math.random() < 0.3) {
scheduleTimer(() => {
state.isBlinking = true;
scheduleTimer(() => {
state.isBlinking = false;
scheduleBlink();
}, 150);
}, 200);
}
else {
scheduleBlink();
}
}, 150);
}, interval);
}
function scheduleSparkle() {
const pause = randInt(1000, 5000);
scheduleTimer(() => {
state.sparkleActive = true;
state.sparkleFrame = 0;
sparkleStep(0);
}, pause);
}
function sparkleStep(step) {
if (step >= SPARKLE_CHARS.length) {
state.sparkleActive = false;
scheduleSparkle();
return;
}
state.sparkleFrame = step;
scheduleTimer(() => {
sparkleStep(step + 1);
}, 200);
}
return {
getFrame() {
// In-place substitutions only: the art grid is never shifted or padded,
// so the frame keeps its line count and its width.
return baseArt.map((line, i) => {
if (state.isBlinking && i === EYE_ROW) {
return applyBlinkToLine(line);
}
if (i === SPARKLE_ROW) {
return applySparkleToLine(line, state.sparkleFrame, state.sparkleActive);
}
return line;
});
},
start() {
scheduleBlink();
scheduleSparkle();
},
stop() {
for (const t of timers) {
clearTimeout(t);
}
timers.length = 0;
},
};
}
/**
* Return the phren art as a single string, optionally indented.
*/
export function renderPhrenArt(indent = "") {
return PHREN_ART.map(line => indent + line).join("\n");
}