kawazu
Version:
kawazu CLI tool for real-time chat in your editor
274 lines (273 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectMessageType = detectMessageType;
exports.formatMessage = formatMessage;
exports.sanitizeMessage = sanitizeMessage;
exports.isSystemMessage = isSystemMessage;
exports.extractNewContent = extractNewContent;
exports.isFileShareCommand = isFileShareCommand;
exports.parseFileShareCommand = parseFileShareCommand;
function detectMessageType(content) {
return content.includes('```') ? 'code' : 'text';
}
function formatMessage(username, content, timestamp, isOwnMessage = false) {
const time = new Date(timestamp).toLocaleTimeString('ja-JP', {
hour: '2-digit',
minute: '2-digit'
});
const userIcon = getUserIcon(username);
const userColor = getUserColor(username);
const isCode = content.includes('```');
if (isCode) {
return formatCodeMessage(username, content, time, userIcon, userColor, isOwnMessage);
}
// 通常のメッセージ
return formatTextMessage(username, content, time, userIcon, userColor, isOwnMessage);
}
function formatTextMessage(username, content, time, icon, color, isOwnMessage) {
// シンプルな左寄せフォーマット: 名前 アイコン 時刻
const wrappedContent = wrapText(content, 60);
// ヘッダー行: 名前 アイコン 時刻(ANSIコードを削除)
const header = `${username} ${icon} ${time}`;
// メッセージ行
const messageLines = wrappedContent.map(line => ` ${line}`);
return `${header}
${messageLines.join('\n')}
`;
}
function getUserIcon(username) {
const icons = ['👤', '👨', '👩', '🧑', '👦', '👧', '🐱', '🐶', '🦊', '🐼', '🤖', '👽', '🎭', '🦄', '🔥'];
const hash = username.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
return icons[hash % icons.length];
}
function getUserColor(username) {
// ANSI色コード(ターミナル対応)
const colors = [
'\x1b[94m', // 明るい青
'\x1b[92m', // 明るい緑
'\x1b[95m', // 明るいマゼンタ
'\x1b[96m', // 明るいシアン
'\x1b[93m', // 明るい黄色
'\x1b[91m', // 明るい赤
'\x1b[97m', // 明るい白
'\x1b[90m', // 明るい黒
];
const hash = username.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
return colors[hash % colors.length];
}
function wrapText(text, maxWidth) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
for (const word of words) {
if ((currentLine + word).length <= maxWidth) {
currentLine += (currentLine ? ' ' : '') + word;
}
else {
if (currentLine) {
lines.push(currentLine);
}
currentLine = word;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines.length ? lines : [''];
}
function formatCodeMessage(username, content, time, icon, color, isOwnMessage) {
const codeBlocks = content.split('```');
// シンプルな左寄せフォーマット: 名前 アイコン 💻 時刻(ANSIコードを削除)
const header = `${username} ${icon} 💻 ${time}`;
let formatted = `${header}\n`;
for (let i = 0; i < codeBlocks.length; i++) {
if (i % 2 === 1) { // コードブロック内
const codeLines = codeBlocks[i].split('\n').filter(line => line.trim());
codeLines.forEach(line => {
formatted += ` ${line}\n`;
});
}
else if (codeBlocks[i].trim()) { // テキスト部分
const textLines = codeBlocks[i].trim().split('\n');
textLines.forEach(line => {
if (line.trim()) {
formatted += ` ${line}\n`;
}
});
}
}
formatted += '\n';
return formatted;
}
function sanitizeMessage(content) {
// 基本的なサニタイズ
return content
.trim()
.replace(/[\r\n]+/g, '\n') // 改行を統一
.substring(0, 10000); // 長さ制限
}
function isSystemMessage(line) {
const trimmed = line.trim();
return trimmed.startsWith('#') || trimmed.startsWith('[') || trimmed.length === 0;
}
function extractNewContent(currentContent, lastContent) {
console.log('🔍 extractNewContent 開始');
// 新しい線形式のマーカーを定義
const inputLineStart = '------------------------------------------------------------------------------>';
const separatorLine = '================================================================================';
console.log('🔍 マーカー定義:', { inputLineStart: inputLineStart.substring(0, 20) + '...', separatorLine: separatorLine.substring(0, 20) + '...' });
// 現在のコンテンツから入力エリアを抽出
const currentInputArea = extractInputAreaFromContent(currentContent);
console.log('🔍 現在の入力エリア:', `"${currentInputArea}"`);
if (!currentInputArea) {
console.log('🔍 現在の入力エリアが見つかりません');
return '';
}
// 前回のコンテンツから入力エリアを抽出
const lastInputArea = lastContent ? extractInputAreaFromContent(lastContent) : '';
console.log('🔍 前回の入力エリア:', `"${lastInputArea}"`);
// 両方のエリアを行に分割してクリーンアップ
const currentLines = cleanupMessageLines(currentInputArea.split('\n'));
const lastLines = lastContent ? cleanupMessageLines(lastInputArea.split('\n')) : [];
console.log('🔍 現在のクリーンな行:', currentLines);
console.log('🔍 前回のクリーンな行:', lastLines);
// 差分を検出して新しいメッセージのみを抽出
const newLines = detectNewLines(currentLines, lastLines);
console.log('🔍 検出された新しい行:', newLines);
// 結果をフィルタリングして返す
const result = newLines.join('\n').trim();
console.log('🔍 extractNewContent 結果:', `"${result}"`);
return result;
}
function extractInputAreaFromContent(content) {
console.log('🔍 extractInputAreaFromContent 開始');
const inputLineStart = '------------------------------------------------------------------------------>';
const separatorLine = '================================================================================';
// 最新の入力線を見つける
const inputLineIndex = content.lastIndexOf(inputLineStart);
console.log('🔍 入力線のインデックス:', inputLineIndex);
if (inputLineIndex === -1) {
console.log('🔍 入力線が見つかりません');
return '';
}
// 入力線より前の部分を取得
const beforeInputLine = content.substring(0, inputLineIndex);
console.log('🔍 入力線より前の文字数:', beforeInputLine.length);
// 最後の区切り線を見つける
const lastSeparatorIndex = beforeInputLine.lastIndexOf(separatorLine);
console.log('🔍 最後の区切り線のインデックス:', lastSeparatorIndex);
if (lastSeparatorIndex === -1) {
console.log('🔍 区切り線が見つかりません');
return '';
}
// 区切り線から入力線までの内容を抽出
const result = beforeInputLine.substring(lastSeparatorIndex + separatorLine.length);
console.log('🔍 抽出された入力エリア:', `"${result}"`);
return result;
}
function cleanupMessageLines(lines) {
console.log('🔍 cleanupMessageLines 開始');
console.log('🔍 元の行数:', lines.length);
console.log('🔍 元の行:', lines);
const result = lines
.map(line => line.trim())
.filter(line => {
if (!line) {
console.log('🔍 空行を除外:', `"${line}"`);
return false;
}
// システムメッセージやガイダンスを除外
if (line.includes('💭 チャットを開始しましょう!')) {
console.log('🔍 システムメッセージを除外:', `"${line}"`);
return false;
}
if (line.includes('メッセージを線の上に書き')) {
console.log('🔍 ガイダンスを除外:', `"${line}"`);
return false;
}
if (line.includes('ファイルを保存すると送信されます')) {
console.log('🔍 ガイダンスを除外:', `"${line}"`);
return false;
}
if (line.includes('Ctrl+C で終了')) {
console.log('🔍 ガイダンスを除外:', `"${line}"`);
return false;
}
// コメント行を除外
if (isSystemMessage(line)) {
console.log('🔍 システムメッセージを除外:', `"${line}"`);
return false;
}
console.log('🔍 有効な行として採用:', `"${line}"`);
return true;
});
console.log('🔍 cleanupMessageLines 結果:', result);
return result;
}
function detectNewLines(currentLines, lastLines) {
console.log('🔍 detectNewLines 開始');
console.log('🔍 現在の行数:', currentLines.length);
console.log('🔍 前回の行数:', lastLines.length);
// 前回の行数より多い場合、新しい行があるということ
if (currentLines.length > lastLines.length) {
const newLines = currentLines.slice(lastLines.length);
console.log('🔍 新しい行を検出(行数増加):', newLines);
return newLines;
}
// 同じ長さの場合、最後の行が変更されているかチェック
if (currentLines.length === lastLines.length && currentLines.length > 0) {
const lastCurrentLine = currentLines[currentLines.length - 1];
const lastPreviousLine = lastLines.length > 0 ? lastLines[lastLines.length - 1] : '';
console.log('🔍 最後の行の比較:');
console.log('🔍 現在の最後の行:', `"${lastCurrentLine}"`);
console.log('🔍 前回の最後の行:', `"${lastPreviousLine}"`);
if (lastCurrentLine !== lastPreviousLine) {
// 最後の行が変更されている場合、その行のみを返す
console.log('🔍 最後の行が変更されました:', [lastCurrentLine]);
return [lastCurrentLine];
}
}
console.log('🔍 新しい行は見つかりませんでした');
return [];
}
function isKawazuCommand(line) {
const trimmed = line.trim();
return trimmed.startsWith('kawazu ') || isFileShareCommand(trimmed);
}
function isFileShareCommand(line) {
const trimmed = line.trim();
return trimmed.startsWith('#share ') || trimmed.startsWith('#approve ') || trimmed.startsWith('#deny ');
}
function parseFileShareCommand(line) {
const trimmed = line.trim();
// #share ファイルパス @user1 @user2 --write
if (trimmed.startsWith('#share ')) {
const parts = trimmed.substring(7).split(' ');
const filePath = parts[0];
const users = parts.filter(p => p.startsWith('@')).map(u => u.substring(1));
const permission = parts.includes('--write') ? 'write' : 'read';
return {
command: 'share',
filePath,
users: users.length > 0 ? users : undefined,
permission
};
}
// #approve トークン
if (trimmed.startsWith('#approve ')) {
const token = trimmed.substring(9).trim();
return {
command: 'approve',
token
};
}
// #deny トークン
if (trimmed.startsWith('#deny ')) {
const token = trimmed.substring(6).trim();
return {
command: 'deny',
token
};
}
return null;
}