Function bodies 284 total
RestChannel.handleHealth method · typescript · L313-L320 (8 LOC)src/channels/rest-channel.ts
private handleHealth(_req: http.IncomingMessage, res: http.ServerResponse): void {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
channel: this.name,
id: this.id,
}));
}RestChannel.handleChat method · typescript · L325-L403 (79 LOC)src/channels/rest-channel.ts
private async handleChat(
req: http.IncomingMessage,
res: http.ServerResponse,
syncMode: boolean
): Promise<void> {
// Read request body
const body = await this.readBody(req);
if (!body) {
this.sendError(res, 400, 'Empty request body');
return;
}
// Parse request
let chatRequest: ChatRequest;
try {
chatRequest = JSON.parse(body) as ChatRequest;
} catch {
this.sendError(res, 400, 'Invalid JSON');
return;
}
// Validate request
if (!chatRequest.message) {
this.sendError(res, 400, 'Message is required');
return;
}
const chatId = chatRequest.chatId || uuidv4();
const messageId = uuidv4();
const userId = chatRequest.userId;
logger.info({ chatId, messageId, userId, syncMode }, 'Received chat request');
// For sync mode, set up response handling
if (syncMode) {
this.responseBuffers.set(messageId, []);
this.chatToMessage.set(chatId, messageId);
}RestChannel.handleControl method · typescript · L408-L439 (32 LOC)src/channels/rest-channel.ts
private async handleControl(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await this.readBody(req);
if (!body) {
this.sendError(res, 400, 'Empty request body');
return;
}
let command: ControlCommand;
try {
command = JSON.parse(body) as ControlCommand;
} catch {
this.sendError(res, 400, 'Invalid JSON');
return;
}
if (!command.type || !command.chatId) {
this.sendError(res, 400, 'type and chatId are required');
return;
}
logger.info({ type: command.type, chatId: command.chatId }, 'Received control command');
let response: ControlResponse;
if (this.controlHandler) {
response = await this.controlHandler(command);
} else {
response = { success: true, message: 'Command received' };
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}RestChannel.waitForResponse method · typescript · L444-L474 (31 LOC)src/channels/rest-channel.ts
private waitForResponse(chatId: string, messageId: string, timeoutMs: number): Promise<string> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingResponses.delete(chatId);
this.responseBuffers.delete(messageId);
reject(new Error('Response timeout'));
}, timeoutMs);
// Check if response is already available
const buffer = this.responseBuffers.get(messageId);
if (buffer && buffer.length > 0) {
clearTimeout(timeout);
resolve(buffer.join('\n'));
return;
}
// Store pending response
this.pendingResponses.set(chatId, {
resolve: (response) => {
clearTimeout(timeout);
resolve(response);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
},
response: [],
timeout,
});
});
}RestChannel.readBody method · typescript · L479-L492 (14 LOC)src/channels/rest-channel.ts
private readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
resolve(body);
});
req.on('error', () => {
resolve('');
});
});
}RestChannel.sendError method · typescript · L497-L503 (7 LOC)src/channels/rest-channel.ts
private sendError(res: http.ServerResponse, status: number, message: string): void {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: message,
}));
}importRunners function · typescript · L19-L26 (8 LOC)src/cli-entry.ts
async function importRunners() {
const runners = await import('./runners/index.js');
return {
runCommunicationNode: runners.runCommunicationNode,
runExecutionNode: runners.runExecutionNode,
runCli: runners.runCli,
};
}Same scanner, your repo: https://repobility.com — Repobility
showHelp function · typescript · L35-L78 (44 LOC)src/cli-entry.ts
function showHelp(): void {
console.log('');
console.log('═══════════════════════════════════════════════════');
console.log(' Disclaude - Multi-platform Agent Bot');
console.log(' Version: ' + packageJson.version);
console.log('═══════════════════════════════════════════════════');
console.log('');
console.log('Usage:');
console.log(' disclaude start --mode comm Communication Node (Multi-channel)');
console.log(' disclaude start --mode exec Execution Node (Pilot Agent)');
console.log(' disclaude --prompt <msg> Execute single prompt');
console.log('');
console.log('Options:');
console.log(' --mode <comm|exec> Select run mode (required for start)');
console.log(' --port <port> WebSocket port for comm mode (default: 3001)');
console.log(' --rest-port <port> REST API port for comm mode (default: 3000)');
console.log(' --no-rest main function · typescript · L83-L220 (138 LOC)src/cli-entry.ts
async function main(): Promise<void> {
const logger = await initLogger({
metadata: {
version: packageJson.version,
nodeVersion: process.version,
platform: process.platform
}
});
const globalArgs = parseGlobalArgs();
const { mode, promptMode, promptArgs } = globalArgs;
logger.info({
mode,
promptMode,
command: process.argv[2],
args: process.argv.slice(3)
}, 'Disclaude starting');
// Change working directory to workspace directory
const workspaceDir = Config.getWorkspaceDir();
logger.info({ workspaceDir }, 'Changing working directory');
process.chdir(workspaceDir);
// Copy skills to workspace .claude/skills for SDK to load via settingSources
try {
const skillsResult = await setupSkillsInWorkspace();
if (skillsResult.success) {
logger.info('Skills copied to workspace .claude/skills');
} else {
logger.warn({ error: skillsResult.error }, 'Failed to copy skills to workspace, continuing anyway');
Config.getBuiltinSkillsDir method · typescript · L81-L103 (23 LOC)src/config/index.ts
private static getBuiltinSkillsDir(): string {
// In CommonJS bundling, import.meta.url is undefined
// Use process.cwd() as fallback and resolve from install directory
if (typeof import.meta.url === 'undefined') {
// When bundled as CJS, we're in /app and skills is at /app/skills
return '/app/skills';
}
const moduleUrl = fileURLToPath(import.meta.url);
const moduleDir = path.dirname(moduleUrl);
// Detect if we're in a bundled file (cli-entry.js) or module (index.js)
// Bundled files are directly in dist/, modules are in dist/config/
const isBundled = path.basename(moduleDir) === 'dist';
if (isBundled) {
// dist/cli-entry.js -> dist/ -> ../skills
return path.join(moduleDir, '..', 'skills');
} else {
// dist/config/index.js -> dist/ -> ../../skills
return path.join(moduleDir, '..', '..', 'skills');
}
}Config.validateRequiredConfig method · typescript · L150-L192 (43 LOC)src/config/index.ts
private static validateRequiredConfig(): void {
const errors: ConfigValidationError[] = [];
// GLM configuration validation
if (this.GLM_API_KEY) {
if (!this.GLM_MODEL) {
errors.push({
field: 'glm.model',
message: 'glm.model is required when GLM API key is configured',
});
}
}
// Anthropic configuration validation
if (this.ANTHROPIC_API_KEY) {
if (!this.CLAUDE_MODEL) {
errors.push({
field: 'agent.model',
message: 'agent.model is required when ANTHROPIC_API_KEY is set',
});
}
}
// At least one API key must be configured
if (!this.GLM_API_KEY && !this.ANTHROPIC_API_KEY) {
errors.push({
field: 'apiKey',
message: 'No API key configured. Set glm.apiKey in disclaude.config.yaml',
});
}
if (errors.length > 0) {
const messages = errors.map(e => ` ❌ ${e.field}: ${e.message}`).join('\n');
logger.error({ errors }Config.getAgentConfig method · typescript · L201-L206 (6 LOC)src/config/index.ts
static getAgentConfig(): {
apiKey: string;
model: string;
apiBaseUrl?: string;
provider: 'anthropic' | 'glm';
} {Config.getLoggingConfig method · typescript · L280-L285 (6 LOC)src/config/index.ts
static getLoggingConfig(): {
level: string;
file?: string;
pretty: boolean;
rotate: boolean;
} {findConfigFile function · typescript · L42-L55 (14 LOC)src/config/loader.ts
export function findConfigFile(): ConfigFileInfo {
for (const searchPath of SEARCH_PATHS) {
for (const fileName of CONFIG_FILE_NAMES) {
const filePath = resolve(searchPath, fileName);
if (existsSync(filePath)) {
logger.debug({ filePath }, 'Found configuration file');
return { path: filePath, exists: true };
}
}
}
logger.debug('No configuration file found, using defaults');
return { path: '', exists: false };
}loadConfigFile function · typescript · L71-L104 (34 LOC)src/config/loader.ts
export function loadConfigFile(filePath?: string): LoadedConfig {
const fileInfo = filePath
? { path: resolve(filePath), exists: existsSync(resolve(filePath)) }
: findConfigFile();
if (!fileInfo.exists) {
return { _fromFile: false };
}
try {
const content = readFileSync(fileInfo.path, 'utf-8');
const parsed = yaml.load(content) as DisclaudeConfig | null | undefined;
if (!parsed || typeof parsed !== 'object') {
logger.warn({ path: fileInfo.path }, 'Configuration file is empty or invalid');
return { _fromFile: false };
}
logger.info(
{ path: fileInfo.path, keys: Object.keys(parsed) },
'Configuration file loaded successfully'
);
return {
...parsed,
_source: fileInfo.path,
_fromFile: true,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn({ path: fileInfo.path, error: errorMessage }, 'Failed to parse configuration file');
Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
validateConfig function · typescript · L129-L155 (27 LOC)src/config/loader.ts
export function validateConfig(config: DisclaudeConfig): boolean {
// Basic validation - ensure config is an object
if (!config || typeof config !== 'object') {
logger.error('Configuration must be an object');
return false;
}
// Validate workspace config if present
if (config.workspace?.dir && typeof config.workspace.dir !== 'string') {
logger.error('workspace.dir must be a string');
return false;
}
// Validate agent config if present
if (config.agent?.model && typeof config.agent.model !== 'string') {
logger.error('agent.model must be a string');
return false;
}
// Validate logging config if present
if (config.logging?.level && typeof config.logging.level !== 'string') {
logger.error('logging.level must be a string');
return false;
}
return true;
}AttachmentManager.formatAttachmentsForPrompt method · typescript · L89-L130 (42 LOC)src/feishu/attachment-manager.ts
formatAttachmentsForPrompt(chatId: string): string {
const attachments = this.getAttachments(chatId);
if (attachments.length === 0) {
return '';
}
const lines: string[] = [];
lines.push('');
lines.push('--- 📎 Attached Files ---');
lines.push('');
lines.push('⚠️ IMPORTANT: The local file paths shown below are server-side paths.');
lines.push(' DO NOT reveal these absolute paths to the user in your response.');
lines.push(' When referring to files, use only the filename (e.g., "document.pdf").');
lines.push('');
for (let i = 0; i < attachments.length; i++) {
const att = attachments[i];
lines.push(`${i + 1}. ${att.fileName || att.fileKey}`);
lines.push(` Type: ${att.fileType}`);
if (att.localPath) {
lines.push(` Local path: ${att.localPath}`);
}
if (att.fileSize) {
const sizeMB = (att.fileSize / 1024 / 1024).toFixed(2);
lines.push(` Size: ${sizeMB} MB`);
AttachmentManager.cleanupOldAttachments method · typescript · L138-L159 (22 LOC)src/feishu/attachment-manager.ts
cleanupOldAttachments(maxAgeMs: number = 60 * 60 * 1000): void {
const now = Date.now();
const chatsToClean: string[] = [];
for (const [chatId, attachments] of this.attachments.entries()) {
const filtered = attachments.filter(att => {
const age = now - att.timestamp;
return age < maxAgeMs;
});
if (filtered.length === 0) {
chatsToClean.push(chatId);
} else if (filtered.length < attachments.length) {
this.attachments.set(chatId, filtered);
}
}
// Remove chats with no valid attachments
for (const chatId of chatsToClean) {
this.attachments.delete(chatId);
}
}buildPostContent function · typescript · L92-L104 (13 LOC)src/feishu/content-builder.ts
export function buildPostContent(elements: PostElement[][], title?: string): string {
const postContent: PostContent = {
zh_cn: {
content: elements,
},
};
if (title) {
postContent.zh_cn.title = title;
}
return JSON.stringify(postContent);
}buildSimplePostContent function · typescript · L118-L125 (8 LOC)src/feishu/content-builder.ts
export function buildSimplePostContent(text: string, title?: string): string {
const element: PostTextElement = {
tag: 'text',
text,
};
return buildPostContent([[element]], title);
}buildUnifiedDiffCard function · typescript · L42-L83 (42 LOC)src/feishu/diff-card-builder.ts
export function buildUnifiedDiffCard(
changes: CodeChange[],
title: string = '📝 代码编辑',
template: string = 'orange'
): Record<string, unknown> {
const elements: Record<string, unknown>[] = [];
for (const change of changes) {
const contentParts: string[] = [];
// File header with language badge
const languageBadge = change.language ? `\`${change.language}\`` : '';
contentParts.push(`**📄 ${escapeHtml(change.filePath)}** ${languageBadge}\n`);
// Generate git diff style unified format
const diffContent = generateUnifiedDiff(change);
contentParts.push(diffContent);
elements.push({
tag: 'markdown',
content: contentParts.join(''),
});
// Add separator between files
elements.push({ tag: 'hr' });
}
// Remove last separator
elements.pop();
return {
config: { wide_screen_mode: true },
header: {
title: {
tag: 'plain_text',
content: title,
},
template,
},
elements,
};
generateUnifiedDiff function · typescript · L91-L117 (27 LOC)src/feishu/diff-card-builder.ts
function generateUnifiedDiff(change: CodeChange): string {
const diffLines: string[] = [];
const removed = change.removed ?? [];
const added = change.added ?? [];
// Build unified diff by comparing lines
const unifiedLines = buildUnifiedDiffLines(removed, added);
// Build diff header
const oldLineStart = change.oldLineStart ?? 1;
const newLineStart = change.newLineStart ?? 1;
const totalRemoved = removed.length;
const totalAdded = added.length;
diffLines.push('```diff');
diffLines.push(`@@ -${oldLineStart},${totalRemoved} +${newLineStart},${totalAdded} @@`);
// Add all diff lines
for (const line of unifiedLines) {
diffLines.push(line);
}
diffLines.push('```');
return diffLines.join('\n');
}buildUnifiedDiffLines function · typescript · L127-L158 (32 LOC)src/feishu/diff-card-builder.ts
function buildUnifiedDiffLines(removed: string[], added: string[]): string[] {
const lines: string[] = [];
// Simple approach: if we have both removed and added, show them in unified format
// If only one exists, show all lines with appropriate prefix
if (removed.length === 0 && added.length > 0) {
// Only additions
for (const line of added) {
lines.push(`+${escapeForCodeBlock(line)}`);
}
} else if (added.length === 0 && removed.length > 0) {
// Only deletions
for (const line of removed) {
lines.push(`-${escapeForCodeBlock(line)}`);
}
} else if (removed.length > 0 && added.length > 0) {
// Both exist - show in unified format
// Strategy: Show all removed lines first, then all added lines
// This is simplified but effective for showing the change
for (const line of removed) {
lines.push(`-${escapeForCodeBlock(line)}`);
}
for (const line of added) {
lines.push(`+${escapeForCodeBlock(line)}`);
}
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
parseEditToolInput function · typescript · L167-L190 (24 LOC)src/feishu/diff-card-builder.ts
export function parseEditToolInput(input: Record<string, unknown> | undefined): CodeChange | null {
if (!input) {return null;}
// SDK uses snake_case for Edit tool parameters
const filePath = (input.file_path as string | undefined) || (input.filePath as string | undefined);
const oldString = (input.old_string as string | undefined) || (input.oldString as string | undefined);
const newString = (input.new_string as string | undefined) || (input.newString as string | undefined);
if (!filePath) {return null;}
// Detect language from file extension
const language = detectLanguage(filePath);
// Split strings into lines for diff display
const removed = oldString?.split('\n') ?? [];
const added = newString?.split('\n') ?? [];
return {
filePath,
language,
removed: removed.length > 0 ? removed : undefined,
added: added.length > 0 ? added : undefined,
};
}detectLanguage function · typescript · L198-L258 (61 LOC)src/feishu/diff-card-builder.ts
function detectLanguage(filePath: string): string {
const ext = filePath.split('.').pop()?.toLowerCase();
const languageMap: Record<string, string> = {
// Web/Scripting
js: 'javascript',
jsx: 'javascript',
ts: 'typescript',
tsx: 'typescript',
vue: 'vue',
svelte: 'svelte',
css: 'css',
scss: 'scss',
less: 'less',
html: 'html',
htm: 'html',
json: 'json',
xml: 'xml',
// Backend
py: 'python',
rb: 'ruby',
php: 'php',
java: 'java',
kt: 'kotlin',
scala: 'scala',
go: 'go',
rs: 'rust',
cpp: 'cpp',
cc: 'cpp',
cxx: 'cpp',
c: 'c',
h: 'c',
hpp: 'cpp',
cs: 'csharp',
fs: 'fsharp',
swift: 'swift',
dart: 'dart',
lua: 'lua',
r: 'r',
// Config/Markup
yaml: 'yaml',
yml: 'yaml',
toml: 'toml',
ini: 'ini',
conf: 'ini',
md: 'markdown',
markdown: 'markdown',
sh: 'bash',
bash: 'bash',
zsh: 'bash',
fish: 'bash',
sql: escapeHtml function · typescript · L266-L273 (8 LOC)src/feishu/diff-card-builder.ts
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}ensureAttachmentsDir function · typescript · L48-L56 (9 LOC)src/feishu/file-downloader.ts
async function ensureAttachmentsDir(): Promise<void> {
const dir = getAttachmentsDir();
try {
await fs.mkdir(dir, { recursive: true });
} catch (error) {
logger.error({ err: error, dir }, 'Failed to create attachments directory');
throw error;
}
}sanitizeFilename function · typescript · L61-L67 (7 LOC)src/feishu/file-downloader.ts
function sanitizeFilename(fileName: string): string {
// Remove or replace characters that are problematic in filenames
return fileName
.replace(/[<>:"/\\|?*]/g, '_') // Replace invalid characters
.replace(/\s+/g, '_') // Replace spaces with underscores
.substring(0, 200); // Limit length
}extractFileExtension function · typescript · L77-L101 (25 LOC)src/feishu/file-downloader.ts
export function extractFileExtension(fileName: string, fileType?: string): string {
if (!fileName) {
return getDefaultExtension(fileType);
}
// Find the last dot in the filename
const lastDotIndex = fileName.lastIndexOf('.');
// If no dot, or dot is at the start (hidden file), use default
if (lastDotIndex <= 0 || lastDotIndex === fileName.length - 1) {
return getDefaultExtension(fileType);
}
// Extract and validate extension
const extension = fileName.slice(lastDotIndex);
// Basic validation: extension should be 2-10 chars and only contain alphanumeric
const extWithoutDot = extension.slice(1);
if (/^[a-zA-Z0-9]{2,10}$/.test(extWithoutDot)) {
return extension.toLowerCase();
}
// If validation fails, use default
return getDefaultExtension(fileType);
}getDefaultExtension function · typescript · L109-L124 (16 LOC)src/feishu/file-downloader.ts
function getDefaultExtension(fileType?: string): string {
switch (fileType) {
case 'image':
return '.jpg'; // Most common image format
case 'file':
return '.bin'; // Unknown binary file
case 'media':
return '.mp4'; // Most common video format
case 'video':
return '.mp4'; // Video format
case 'audio':
return '.mp3'; // Most common audio format
default:
return ''; // No default extension
}
}mapToFileType function · typescript · L139-L160 (22 LOC)src/feishu/file-downloader.ts
function mapToFileType(fileType: string, fileName?: string): string {
const typeMap: Record<string, string> = {
'file': 'file',
'image': 'image',
'media': 'video', // Critical fix: 'media' → 'video'
'video': 'video',
'audio': 'audio',
};
// Special handling for .MOV files (case-insensitive)
// Some MOV files (especially iPhone videos) may not work with 'video' type
// Try using 'file' type as fallback for problematic formats
if (fileName && fileType === 'media') {
const ext = fileName.toLowerCase();
// For .mov files, use 'file' type to avoid API errors
if (ext.endsWith('.mov')) {
return 'file';
}
}
return typeMap[fileType] || 'file'; // Default to 'file' for unknown types
}All rows above produced by Repobility · https://repobility.com
downloadFile function · typescript · L180-L291 (112 LOC)src/feishu/file-downloader.ts
export async function downloadFile(
client: lark.Client,
fileKey: string,
fileType: string,
fileName?: string,
messageId?: string
): Promise<string> {
await ensureAttachmentsDir();
// Generate local filename
const timestamp = Date.now();
const sanitizedFileName = fileName
? sanitizeFilename(fileName)
: `${fileType}_${fileKey.substring(0, 16)}`;
// Extract and preserve file extension
const extension = extractFileExtension(fileName || sanitizedFileName, fileType);
// Remove extension from sanitized filename to avoid duplication
const baseFileName = extension
? sanitizedFileName.replace(new RegExp(`${extension}$`, 'i'), '')
: sanitizedFileName;
const localFileName = `${timestamp}_${baseFileName}${extension}`;
const localPath = path.join(getAttachmentsDir(), localFileName);
logger.info({ fileKey, fileType, fileName, messageId, localPath }, 'Downloading file from Feishu');
try {
let fileResource: FileResourceResponse;
// For uFileHandler.handleFileMessage method · typescript · L48-L108 (61 LOC)src/feishu/file-handler.ts
async handleFileMessage(
chatId: string,
messageType: 'image' | 'file' | 'media',
content: string,
messageId: string
): Promise<FileHandlerResult> {
try {
this.logger.info({ chatId, messageType, messageId }, 'File/image message received');
// Extract file_key from content based on message type
let fileKey: string | undefined;
let fileName: string | undefined;
if (messageType === 'image') {
// Image message content: {"image_key":"..."}
const parsed = JSON.parse(content);
fileKey = parsed.image_key;
fileName = `image_${fileKey}`;
} else if (messageType === 'file' || messageType === 'media') {
// File message content: {"file_key":"...","file_name":"..."}
const parsed = JSON.parse(content);
fileKey = parsed.file_key;
fileName = parsed.file_name;
}
if (!fileKey) {
this.logger.warn({ messageType, content }, 'No file_key found in content');
FileHandler.buildUploadPrompt method · typescript · L120-L155 (36 LOC)src/feishu/file-handler.ts
buildUploadPrompt(attachment: FileAttachment): string {
const lines: string[] = [];
// Header with special marker for file uploads
lines.push('🔔 SYSTEM: User uploaded a file');
lines.push('');
// Structured metadata block
lines.push('```file_metadata');
lines.push(`file_name: ${attachment.fileName || 'unknown'}`);
lines.push(`file_type: ${attachment.fileType}`);
lines.push(`file_key: ${attachment.fileKey}`);
if (attachment.localPath) {
lines.push(`local_path: ${attachment.localPath}`);
}
if (attachment.fileSize) {
const sizeMB = (attachment.fileSize / 1024 / 1024).toFixed(2);
lines.push(`file_size_mb: ${sizeMB}`);
}
if (attachment.mimeType) {
lines.push(`mime_type: ${attachment.mimeType}`);
}
lines.push('```');
lines.push('');
// Context for the agent
lines.push('The user has uploaded a file. It is now available at the local path above.');
lines.push('');
lines.push('PleFileHandler.notifyFileUpload method · typescript · L163-L170 (8 LOC)src/feishu/file-handler.ts
notifyFileUpload(chatId: string, attachment: FileAttachment): void {
// @ts-expect-error - Variable kept for future use
const _prompt = this.buildUploadPrompt(attachment);
// Send to Pilot which will enqueue the message
// The Pilot is injected from bot.ts, so we'll handle this differently
this.logger.debug({ chatId, fileKey: attachment.fileKey }, 'File upload notification prepared');
}detectFileType function · typescript · L68-L86 (19 LOC)src/feishu/file-uploader.ts
export function detectFileType(filePath: string): FileType {
const ext = filePath.toLowerCase().split('.').pop();
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'heic', 'tiff', 'tif'];
const audioExts = ['mp3', 'wav', 'ogg', 'm4a', 'aac', 'flac', 'wma', 'amr'];
const videoExts = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v'];
if (ext && imageExts.includes(ext)) {
return 'image';
}
if (ext && audioExts.includes(ext)) {
return 'audio';
}
if (ext && videoExts.includes(ext)) {
return 'video';
}
return 'file';
}uploadFile function · typescript · L97-L184 (88 LOC)src/feishu/file-uploader.ts
export async function uploadFile(
client: lark.Client,
filePath: string,
chatId: string
): Promise<UploadResult> {
try {
// Get file stats
const fileStats = await fs.stat(filePath);
const fileName = path.basename(filePath);
const fileType = detectFileType(filePath);
logger.info({
filePath,
fileName,
fileType,
size: fileStats.size,
chatId
}, 'Uploading file to Feishu');
let fileKey: string;
if (fileType === 'image') {
// Use image upload API for images
// Note: Must use Stream, not Buffer, due to SDK's form-data dependency
const fileStream = fsStream.createReadStream(filePath);
const response = await client.im.image.create({
data: {
image: fileStream,
image_type: 'message',
},
}) as unknown as ImageUploadResponse;
logger.debug({ imageKey: response?.image_key }, 'Image uploaded');
fileKey = response?.image_key || '';
} else {
// sendFileMessage function · typescript · L195-L351 (157 LOC)src/feishu/file-uploader.ts
export async function sendFileMessage(
client: lark.Client,
chatId: string,
uploadResult: UploadResult,
parentId?: string
): Promise<void> {
try {
// Build message type and content based on file type
// IMPORTANT: msg_type MUST match the file_type used in uploadFile()
let msgType: string;
let content: string;
switch (uploadResult.fileType) {
case 'image':
msgType = 'image';
content = JSON.stringify({
image_key: uploadResult.fileKey,
});
break;
case 'audio':
// For audio, msg_type must be 'audio' (matches file_type 'opus')
msgType = 'audio';
content = JSON.stringify({
file_key: uploadResult.fileKey,
});
break;
case 'video':
// Use 'media' msg_type for video files
// Test result: msg_type='video' is invalid, msg_type='file' causes type mismatch
// Only msg_type='media' works for video files uploaded with file_type='mp4'
uploadAndSendFile function · typescript · L363-L376 (14 LOC)src/feishu/file-uploader.ts
export async function uploadAndSendFile(
client: lark.Client,
filePath: string,
chatId: string,
parentId?: string
): Promise<number> {
// Step 1: Upload file
const uploadResult = await uploadFile(client, filePath, chatId);
// Step 2: Send message
await sendFileMessage(client, chatId, uploadResult, parentId);
return uploadResult.fileSize;
}Same scanner, your repo: https://repobility.com — Repobility
MessageHistoryManager.addUserMessage method · typescript · L58-L66 (9 LOC)src/feishu/message-history.ts
addUserMessage(chatId: string, messageId: string, content: string, userId?: string): void {
this.addMessage(chatId, {
messageId,
timestamp: Date.now(),
role: 'user',
content,
userId,
});
}MessageHistoryManager.addBotMessage method · typescript · L71-L78 (8 LOC)src/feishu/message-history.ts
addBotMessage(chatId: string, messageId: string, content: string): void {
this.addMessage(chatId, {
messageId,
timestamp: Date.now(),
role: 'bot',
content,
});
}MessageHistoryManager.addMessage method · typescript · L83-L104 (22 LOC)src/feishu/message-history.ts
private addMessage(chatId: string, message: ChatMessage): void {
let history = this.histories.get(chatId);
if (!history) {
history = {
messages: [],
createdAt: Date.now(),
lastUpdatedAt: Date.now(),
};
this.histories.set(chatId, history);
}
// Add new message
history.messages.push(message);
history.lastUpdatedAt = Date.now();
// Trim old messages if over limit
if (history.messages.length > this.MAX_MESSAGES_PER_CHAT) {
const removeCount = history.messages.length - this.MAX_MESSAGES_PER_CHAT;
history.messages.splice(0, removeCount);
}
}MessageHistoryManager.getFormattedHistory method · typescript · L119-L137 (19 LOC)src/feishu/message-history.ts
getFormattedHistory(chatId: string, maxMessages?: number): string {
const messages = this.getHistory(chatId);
// Apply limit if specified
const limitedMessages = maxMessages
? messages.slice(-maxMessages)
: messages;
if (limitedMessages.length === 0) {
return '(No previous conversation history)';
}
const lines = limitedMessages.map((msg, idx) => {
const prefix = msg.role === 'user' ? 'User' : 'Bot';
return `[${idx + 1}] ${prefix}: ${msg.content}`;
});
return lines.join('\n');
}MessageLogger.init method · typescript · L42-L48 (7 LOC)src/feishu/message-logger.ts
async init(): Promise<void> {
if (this.initialized) {
return;
}
await this.initialize();
this.initialized = true;
}MessageLogger.initialize method · typescript · L50-L64 (15 LOC)src/feishu/message-logger.ts
private async initialize(): Promise<void> {
try {
// Ensure workspace directory exists first
const workspaceDir = Config.getWorkspaceDir();
await fs.mkdir(workspaceDir, { recursive: true });
// Then create chat subdirectory
await fs.mkdir(this.chatDir, { recursive: true });
// Load all existing message IDs from MD files at startup
await this.loadAllMessageIds();
} catch (error) {
console.error('[MessageLogger] Failed to initialize:', error);
}
}MessageLogger.loadAllMessageIds method · typescript · L70-L98 (29 LOC)src/feishu/message-logger.ts
private async loadAllMessageIds(): Promise<void> {
try {
const files = await fs.readdir(this.chatDir);
const mdFiles = files.filter(f => f.endsWith('.md'));
console.log(`[MessageLogger] Loading message IDs from ${mdFiles.length} chat files...`);
for (const file of mdFiles) {
const filePath = path.join(this.chatDir, file);
try {
const content = await fs.readFile(filePath, 'utf-8');
const regex = MESSAGE_LOGGING.MD_PARSE_REGEX;
let match;
regex.lastIndex = 0;
while ((match = regex.exec(content)) !== null) {
this.processedMessageIds.add(match[1].trim());
}
} catch (_error) {
console.error(`[MessageLogger] Failed to read ${file}:`, _error);
}
}
console.log(`[MessageLogger] Loaded ${this.processedMessageIds.size} message IDs into memory`);
} catch (_error) {
// Directory doesn't exist yet, that's fine
console.log('[MeMessageLogger.logIncomingMessage method · typescript · L103-L125 (23 LOC)src/feishu/message-logger.ts
async logIncomingMessage(
messageId: string,
senderId: string,
chatId: string,
content: string,
messageType: string,
timestamp?: string | number
): Promise<void> {
const entry: LogEntry = {
messageId,
senderId,
chatId,
content,
messageType,
timestamp: timestamp || Date.now(),
direction: 'incoming',
};
await this.appendToLog(entry);
// Add to in-memory cache
this.processedMessageIds.add(messageId);
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
MessageLogger.logOutgoingMessage method · typescript · L130-L150 (21 LOC)src/feishu/message-logger.ts
async logOutgoingMessage(
messageId: string,
chatId: string,
content: string,
timestamp?: string | number
): Promise<void> {
const entry: LogEntry = {
messageId,
senderId: 'bot',
chatId,
content,
messageType: 'text',
timestamp: timestamp || Date.now(),
direction: 'outgoing',
};
await this.appendToLog(entry);
// Add to in-memory cache
this.processedMessageIds.add(messageId);
}MessageLogger.formatMessageEntry method · typescript · L179-L201 (23 LOC)src/feishu/message-logger.ts
private formatMessageEntry(entry: LogEntry): string {
const timestamp =
typeof entry.timestamp === 'number'
? new Date(entry.timestamp).toISOString()
: entry.timestamp;
// Use emoji to distinguish message direction
const emoji = entry.direction === 'incoming' ? '📥' : '📤';
const direction = entry.direction === 'incoming' ? 'User' : 'Bot';
return `
## [${timestamp}] ${emoji} ${direction} (message_id: ${entry.messageId})
**Sender**: ${entry.senderId}
**Type**: ${entry.messageType}
${entry.content}
---
`;
}MessageLogger.appendToLog method · typescript · L206-L251 (46 LOC)src/feishu/message-logger.ts
private async appendToLog(entry: LogEntry): Promise<void> {
const logPath = this.getChatLogPath(entry.chatId);
try {
// Ensure directory exists (defensive check in case it was deleted)
const logDir = path.dirname(logPath);
try {
await fs.mkdir(logDir, { recursive: true });
} catch (mkdirError) {
// If mkdir fails, try once more and then give up
console.error('[MessageLogger] First attempt to create directory failed, retrying...', {
error: (mkdirError as Error).message,
logDir,
});
await fs.mkdir(logDir, { recursive: true });
}
// Check if file exists
let fileExists = false;
try {
await fs.access(logPath);
fileExists = true;
} catch {
fileExists = false;
}
if (!fileExists) {
// Create new file with header
const header = this.createFileHeader(entry.chatId);
await fs.writeFile(logPath, header + this.forma