← back to hs3180__disclaude

Function bodies 284 total

All specs Real LLM only Function bodies
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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}
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 u
FileHandler.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('Ple
FileHandler.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('[Me
MessageLogger.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
‹ prevpage 2 / 6next ›