Skip to content
Merged
17 changes: 17 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ tgcli metadata refresh --chat <id|@username> --force --json --timeout 30s
tgcli metadata refresh --only-missing --limit 50 --json --timeout 90s
```

### Folders

```bash
tgcli folders list --json --timeout 30s
tgcli folders show <name|id> --json --timeout 30s
tgcli folders show <name|id> --resolve --json --timeout 30s
tgcli folders create --title "Name" --emoji "🤖" --json --timeout 30s
tgcli folders edit <name|id> --title "New Name" --json --timeout 30s
tgcli folders delete <name|id> --json --timeout 30s
tgcli folders order <id1> <id2> <id3> --json --timeout 30s
tgcli folders add-chat <folder> --chat <id> --json --timeout 30s
tgcli folders remove-chat <folder> --chat <id> --json --timeout 30s
tgcli folders join --link "https://t.me/addlist/slug" --json --timeout 30s
```

Use `--resolve` with `folders show` to resolve peer IDs to readable channel/user names (slower, requires API calls per peer). Without `--resolve`, peers are shown as typed IDs (e.g., `channel:123`).

### Sync Jobs

```bash
Expand Down
46 changes: 40 additions & 6 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ function buildProgram() {
.command('show')
.description('Show folder details')
.argument('<folder>', 'Folder ID or title')
.action(withGlobalOptions((globalFlags, folder) => runFoldersShow(globalFlags, folder)));
.option('--resolve', 'Resolve peer IDs to names (slower, requires API calls)')
.action(withGlobalOptions((globalFlags, folder, opts) => runFoldersShow(globalFlags, folder, opts)));
folders
.command('create')
.description('Create a new folder')
Expand Down Expand Up @@ -3708,7 +3709,7 @@ async function runFoldersList(globalFlags) {
}, timeoutMs);
}

async function runFoldersShow(globalFlags, folder) {
async function runFoldersShow(globalFlags, folder, opts = {}) {
const timeoutMs = globalFlags.timeoutMs;
return runWithTimeout(async () => {
const storeDir = resolveStoreDir();
Expand All @@ -3718,7 +3719,7 @@ async function runFoldersShow(globalFlags, folder) {
if (!(await telegramClient.isAuthorized().catch(() => false))) {
throw new Error('Not authenticated. Run `tgcli auth` first.');
}
const info = await telegramClient.showFolder(folder);
const info = await telegramClient.showFolder(folder, { resolve: opts.resolve });
if (globalFlags.json) {
writeJson(info);
} else {
Expand All @@ -3728,9 +3729,42 @@ async function runFoldersShow(globalFlags, folder) {
if (flags.length) console.log(` includes: ${flags.join(', ')}`);
const excludes = ['excludeMuted', 'excludeRead', 'excludeArchived'].filter((f) => info[f]);
if (excludes.length) console.log(` excludes: ${excludes.join(', ')}`);
if (info.includePeers?.length) console.log(` peers: ${info.includePeers.length} included`);
if (info.excludePeers?.length) console.log(` excluded peers: ${info.excludePeers.length}`);
if (info.pinnedPeers?.length) console.log(` pinned peers: ${info.pinnedPeers.length}`);

if (opts.resolve) {
// Group peers by type and show with names
const printResolvedPeers = (peers, label) => {
if (!peers?.length) return;
if (label) console.log(` ${label}:`);
const indent = label ? ' ' : ' ';
const grouped = {};
for (const p of peers) {
const group = p.type + 's';
if (!grouped[group]) grouped[group] = [];
const displayName = p.name ?? p.title ?? '(unresolved)';
grouped[group].push(`${displayName} (${p.id})`);
}
for (const [group, items] of Object.entries(grouped)) {
console.log(`${indent}${group}:`);
for (const item of items) console.log(`${indent} - ${item}`);
}
};
printResolvedPeers(info.includePeers);
printResolvedPeers(info.excludePeers, 'excluded');
printResolvedPeers(info.pinnedPeers, 'pinned');
} else {
// Show typed ID list
const printPeers = (peers, label) => {
if (!peers?.length) {
console.log(` ${label}: (none)`);
return;
}
console.log(` ${label}:`);
for (const p of peers) console.log(` - ${p.type}:${p.id}`);
};
printPeers(info.includePeers, 'includePeers');
printPeers(info.excludePeers, 'excludePeers');
printPeers(info.pinnedPeers, 'pinnedPeers');
}
}
} finally {
await messageSyncService.shutdown();
Expand Down
109 changes: 109 additions & 0 deletions docs/plans/2026-03-13-folders-show-resolve-peers-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Design: folders show — resolve peer IDs to names

**Issue:** https://github.com/dapi/tgcli/issues/19
**Date:** 2026-03-13
**Status:** Approved

## Problem

`tgcli folders show AI --json` returns `includePeers` as raw `inputPeerChannel`/`inputPeerUser` objects with numeric IDs and accessHash. Impossible to understand which channels/chats/users belong to a folder without manual resolution.

## Solution

Add `--resolve` flag to `folders show` that resolves peer IDs to readable names.

### Approach: Resolve in showFolder (approach A)

Minimal changes, uses existing `getPeerMetadata()` pattern infrastructure.

## Scope

### In scope
- `telegram-client.js`: `_normalizePeer()`, `_resolvePeerName()`, updated `showFolder(idOrName, { resolve })`
- `cli.js`: `--resolve` flag in `folders show`, updated text output
- `SKILL.md`: Folders section with `folders show --resolve` documentation
- Tests for `_normalizePeer()` and `showFolder`

### Out of scope
- MCP server (`mcp-server.js`) — separate issue
- Caching peer names
- Batch API resolution

## Design

### Domain layer (`telegram-client.js`)

#### `showFolder(idOrName, options = {})`

Add `options.resolve` parameter (default: false).

**Without resolve (default):** each peer in `includePeers`/`excludePeers`/`pinnedPeers` is normalized from raw MTCute object to:
```js
{ type: "channel"|"user"|"chat", id: Number }
```

Uses `_extractPeerId()` + type detection (`userId` → user, `channelId` → channel, `chatId` → chat). Fast, no API calls.

**With resolve:** additionally calls lightweight `getChat()`/`getFullUser()` (NOT `getFullChat`) for each peer and adds `title`/`name` field:
```js
{ type: "channel", id: -1001951583351, title: "ИИшница" }
{ type: "user", id: 272066824, name: "Иван Иванов" }
```

#### New private methods

- `_normalizePeer(peer)` — extracts type + id from raw MTCute peer object
- `_resolvePeerName(type, id)` — lightweight name resolution via `getChat()`/`getFullUser()` only

### CLI layer (`cli.js`)

#### Flag `--resolve`

Boolean option on `folders show` command (default: false). Passed to `telegramClient.showFolder(folder, { resolve })`.

#### Text output without `--resolve`

```
AI (id=38, type=filter)
emoji: 🤖
includes: groups, broadcasts
includePeers:
- channel:-1001951583351
- channel:-1002273349814
- user:272066824
excludePeers: (none)
pinnedPeers: (none)
```

#### Text output with `--resolve`

```
AI (id=38, type=filter)
emoji: 🤖
includes: groups, broadcasts
channels:
- ИИшница (-1001951583351)
- Refat Talks: Tech & AI (-1002273349814)
users:
- Иван Иванов (272066824)
```

#### JSON output

Always contains normalized peers (not raw). With `--resolve` adds `title`/`name` fields.

### Error handling

- If a peer cannot be resolved (deleted channel, banned user, no access): don't fail the command
- Show `{ type: "channel", id: -100..., title: "(unresolved)" }`
- In text output: `- (unresolved) (-100...)`
- Empty peer lists: `(none)` in text, `[]` in JSON

### SKILL.md

Add Folders section documenting:
```bash
tgcli folders list --json --timeout 30s
tgcli folders show <name|id> --json --timeout 30s
tgcli folders show <name|id> --resolve --json --timeout 30s
```
Loading
Loading