Skip to content

feat: add tgcli folders commands for Chat Folders (CRUD + shared folders) #3

Description

@dapi

Summary

Add full CRUD support for Telegram Chat Folders (Dialog Filters) via tgcli folders commands. Includes support for shared/chatlist folders.

Proposed Commands

tgcli folders list                          # List all folders
tgcli folders show <id|name>                # Folder details (chats, filters)
tgcli folders create --title <name>         # Create folder
tgcli folders edit <id|name>                # Edit folder
tgcli folders delete <id|name>              # Delete folder
tgcli folders reorder --ids <id1,id2,...>   # Reorder folders
tgcli folders chats add <id|name> --chat <chatId>    # Add chat to folder
tgcli folders chats remove <id|name> --chat <chatId> # Remove chat from folder
tgcli folders join <invite-link>            # Join shared folder

Create/Edit Options

Flag Description
--title Folder name (max 12 chars)
--emoji Emoji icon
--include-contacts Include contacts
--include-non-contacts Include non-contacts
--include-groups Include groups
--include-channels Include channels
--include-bots Include bots
--exclude-muted Exclude muted
--exclude-read Exclude read
--exclude-archived Exclude archived
--chat Chat to include (repeatable)
--exclude-chat Chat to exclude (repeatable)
--pin-chat Pin chat in folder

Technical Notes

  • All required methods already available in @mtcute/core: getFolders, createFolder, editFolder, deleteFolder, findFolder, setFoldersOrder, joinChatlist
  • Pattern follows existing channels/groups/contacts commands
  • Folder resolution by ID or title (like existing chat resolution patterns)
  • show command needs to resolve included peers to display chat names
  • Listing chats in a folder is slow (Telegram API limitation: must fetch all dialogs and filter)

Implementation Plan

Task 1: Add folder domain methods to telegram-client.js

Files: Modify telegram-client.js (add methods after existing listForumTopics at ~line 1196)

Methods to add:

  • getFolders() — list all folders with normalized output
  • findFolder(idOrName) — resolve folder by numeric ID or title string
  • showFolder(idOrName) — detailed folder info with filter flags and peer lists
  • createFolder(options) — create folder with title, emoji, filter flags, peer lists
  • editFolder(idOrName, modification) — modify existing folder properties
  • deleteFolder(idOrName) — delete folder by ID or title
  • setFoldersOrder(ids) — reorder folders
  • addChatToFolder(idOrName, chatId) — append chat to folder's includePeers
  • removeChatFromFolder(idOrName, chatId) — remove chat from folder's includePeers
  • joinChatlist(link) — join shared folder via invite link
async getFolders() {
  await this.ensureLogin();
  const result = await this.client.getFolders();
  return result.filters.map((f) => {
    if (f._ === 'dialogFilterDefault') return { id: 0, title: 'All Chats', type: 'default' };
    return {
      id: f.id,
      title: typeof f.title === 'string' ? f.title : f.title.text,
      emoji: f.emoticon ?? null,
      color: f.color ?? null,
      type: f._ === 'dialogFilterChatlist' ? 'chatlist' : 'filter',
      contacts: f.contacts ?? false,
      nonContacts: f.nonContacts ?? false,
      groups: f.groups ?? false,
      broadcasts: f.broadcasts ?? false,
      bots: f.bots ?? false,
      excludeMuted: f.excludeMuted ?? false,
      excludeRead: f.excludeRead ?? false,
      excludeArchived: f.excludeArchived ?? false,
      includePeers: f.includePeers?.length ?? 0,
      excludePeers: f.excludePeers?.length ?? 0,
      pinnedPeers: f.pinnedPeers?.length ?? 0,
    };
  });
}

async findFolder(idOrName) {
  await this.ensureLogin();
  const id = Number(idOrName);
  if (!isNaN(id)) return this.client.findFolder({ id });
  return this.client.findFolder({ title: String(idOrName) });
}

async showFolder(idOrName) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  return {
    id: folder.id,
    title: typeof folder.title === 'string' ? folder.title : folder.title.text,
    emoji: folder.emoticon ?? null, color: folder.color ?? null,
    type: folder._ === 'dialogFilterChatlist' ? 'chatlist' : 'filter',
    contacts: folder.contacts ?? false, nonContacts: folder.nonContacts ?? false,
    groups: folder.groups ?? false, broadcasts: folder.broadcasts ?? false, bots: folder.bots ?? false,
    excludeMuted: folder.excludeMuted ?? false, excludeRead: folder.excludeRead ?? false, excludeArchived: folder.excludeArchived ?? false,
    includePeers: folder.includePeers ?? [], excludePeers: folder.excludePeers ?? [], pinnedPeers: folder.pinnedPeers ?? [],
  };
}

async createFolder(options) {
  await this.ensureLogin();
  const params = { title: options.title };
  if (options.emoji) params.emoticon = options.emoji;
  if (options.contacts) params.contacts = true;
  if (options.nonContacts) params.nonContacts = true;
  if (options.groups) params.groups = true;
  if (options.broadcasts) params.broadcasts = true;
  if (options.bots) params.bots = true;
  if (options.excludeMuted) params.excludeMuted = true;
  if (options.excludeRead) params.excludeRead = true;
  if (options.excludeArchived) params.excludeArchived = true;
  if (options.includePeers?.length) params.includePeers = options.includePeers;
  if (options.excludePeers?.length) params.excludePeers = options.excludePeers;
  if (options.pinnedPeers?.length) params.pinnedPeers = options.pinnedPeers;
  const result = await this.client.createFolder(params);
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text };
}

async editFolder(idOrName, modification) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const mod = {};
  if (modification.title !== undefined) mod.title = modification.title;
  if (modification.emoji !== undefined) mod.emoticon = modification.emoji;
  if (modification.contacts !== undefined) mod.contacts = modification.contacts;
  if (modification.nonContacts !== undefined) mod.nonContacts = modification.nonContacts;
  if (modification.groups !== undefined) mod.groups = modification.groups;
  if (modification.broadcasts !== undefined) mod.broadcasts = modification.broadcasts;
  if (modification.bots !== undefined) mod.bots = modification.bots;
  if (modification.excludeMuted !== undefined) mod.excludeMuted = modification.excludeMuted;
  if (modification.excludeRead !== undefined) mod.excludeRead = modification.excludeRead;
  if (modification.excludeArchived !== undefined) mod.excludeArchived = modification.excludeArchived;
  if (modification.includePeers !== undefined) mod.includePeers = modification.includePeers;
  if (modification.excludePeers !== undefined) mod.excludePeers = modification.excludePeers;
  if (modification.pinnedPeers !== undefined) mod.pinnedPeers = modification.pinnedPeers;
  const result = await this.client.editFolder({ folder, modification: mod });
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text };
}

async deleteFolder(idOrName) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  await this.client.deleteFolder(folder.id);
  return { deleted: true, id: folder.id };
}

async setFoldersOrder(ids) {
  await this.ensureLogin();
  await this.client.setFoldersOrder(ids.map(Number));
  return { ok: true };
}

async addChatToFolder(idOrName, chatId) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const peers = folder.includePeers ? [...folder.includePeers] : [];
  peers.push(chatId);
  await this.client.editFolder({ folder, modification: { includePeers: peers } });
  return { ok: true, folderId: folder.id };
}

async removeChatFromFolder(idOrName, chatId) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const chatIdStr = String(chatId);
  const peers = (folder.includePeers ?? []).filter((p) => {
    const peerId = typeof p === 'object' && p !== null ? String(p.userId ?? p.channelId ?? p.chatId ?? '') : String(p);
    return peerId !== chatIdStr;
  });
  await this.client.editFolder({ folder, modification: { includePeers: peers } });
  return { ok: true, folderId: folder.id };
}

async joinChatlist(link) {
  await this.ensureLogin();
  const result = await this.client.joinChatlist(link);
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text, type: 'chatlist' };
}

Commit: feat(folders): add folder domain methods to telegram-client


Task 2: Add CLI subcommands in cli.js

Files: Modify cli.js (insert folders block before disableHelpCommand(program) at ~line 408; add handler functions)

Commander subcommands: folders list, folders show <folder>, folders create, folders edit <folder>, folders delete <folder>, folders reorder, folders join <link>, folders chats add <folder>, folders chats remove <folder>.

Handler functions follow same pattern as runGroupsList/runGroupsInfo: resolveStoreDiracquireReadLockcreateServices → auth check → call domain method → writeJson/console output → cleanup in finally.

Commit: feat(folders): add CLI subcommands for folder management


Task 3: Add MCP tools in mcp-server.js

Files: Modify mcp-server.js (add Zod schemas near top ~line 150, register tools near end ~line 1270)

9 MCP tools: listFolders, showFolder, createFolder, editFolder, deleteFolder, reorderFolders, addChatToFolder, removeChatFromFolder, joinChatlist.

Pattern follows existing server.tool(name, description, schema, handler) with await telegramClient.ensureLogin() and JSON response.

Commit: feat(folders): add MCP tools for folder management


Task 4: Smoke test all commands

node cli.js folders list --json --timeout 30s
node cli.js folders create --title "Test" --emoji "🧪" --json --timeout 30s
node cli.js folders show <id> --json --timeout 30s
node cli.js folders delete <id> --json --timeout 30s
node cli.js folders --help

Task 5: Update tgcli skill documentation

Add ### Folders section to skill file:

tgcli folders list --json --timeout 30s
tgcli folders show <id|name> --json --timeout 30s
tgcli folders create --title "AI channels" --emoji "🤖" --include-channels --json --timeout 30s
tgcli folders edit <id|name> --title "New name" --json --timeout 30s
tgcli folders delete <id|name> --json --timeout 30s
tgcli folders reorder --ids 1,2,3 --json --timeout 30s
tgcli folders chats add <id|name> --chat <chatId> --json --timeout 30s
tgcli folders chats remove <id|name> --chat <chatId> --json --timeout 30s
tgcli folders join <invite-link> --json --timeout 30s

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions