writing-methods

Use when adding or modifying high-level client methods in packages/core/src/highlevel/methods/. Covers function signatures, codegen annotations, peer resolution, update handling, pagination, and all common patterns.

451 stars

Best use case

writing-methods is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Use when adding or modifying high-level client methods in packages/core/src/highlevel/methods/. Covers function signatures, codegen annotations, peer resolution, update handling, pagination, and all common patterns.

Teams using writing-methods should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/writing-methods/SKILL.md --create-dirs "https://raw.githubusercontent.com/mtcute/mtcute/main/.claude/skills/writing-methods/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/writing-methods/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How writing-methods Compares

Feature / Agentwriting-methodsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Use when adding or modifying high-level client methods in packages/core/src/highlevel/methods/. Covers function signatures, codegen annotations, peer resolution, update handling, pagination, and all common patterns.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Writing High-Level Methods

Guide for adding/modifying methods in `packages/core/src/highlevel/methods/`.
Also read the `using-mtcute` skill for more information.

## TL Schema Lookup

Before writing a method, look up the TL constructor(s) you need:

```bash
node .claude/skills/using-mtcute/tools/get-constructor.ts <name>
# e.g. node packages/tl/scripts/get-constructor.ts messages.sendMessage
```

This prints TL definition, TypeScript type, union info, and return type.

## Function Signature Convention

Every exported method function follows this pattern:

```ts
import type { ITelegramClient } from '../../client.types.js'

export async function methodName(
  client: ITelegramClient,       // always first arg
  primaryId: InputPeerLike,      // positional: primary identifiers
  secondaryId: number,           // positional: other required args
  params?: {                     // trailing optional params object
    optionalField?: SomeType
  },
): Promise<ReturnType> { ... }
```

Rules:
- First arg is always `client: ITelegramClient` (stripped by codegen for the class method)
- Primary identifiers (chatId, userId, messageIds) are positional
- Optional/secondary params go in a single trailing `params?` object
- When 3+ required params of similar importance exist, consolidate into a single `params` object (see `editAdminRights`)
- All exports need explicit return types (`isolatedDeclarations: true`)
- Generators use `AsyncIterableIterator<T>` return type

## Codegen Annotations

Files are processed by `packages/core/scripts/generate-client.cjs`. Annotations are `// @annotation` comments on the line before the statement.

### `// @copy`
Copies the import/statement into generated `client.ts`. Used ONLY in `_imports.ts` for shared imports.

```ts
// @copy
import { tl } from '../../tl/index.js'
```

### `// @exported`
Re-exports a type from the generated `methods.ts`. Use for params interfaces and offset types.

```ts
// @exported
export interface DeleteMessagesParams { ... }
```

### `// @available=user|bot|both`
Controls the `**Available**:` JSDoc annotation. If absent, auto-detected from which TL methods are called. Do not add on your own.

### `// @alias=name1,name2`
Creates additional class method aliases.

```ts
// @alias=deleteSupergroup
export async function deleteChannel(...) { ... }
```

### `// @skip`
Excludes from client codegen entirely.

### `// @internal` + `// @noemit`
`@internal` marks as private in generated interface. `@noemit` excludes from `methods.ts` re-export. Combine for internal helpers.

```ts
/**
 * @internal
 * @noemit
 */
export function _findMessageInUpdate(...) { ... }
```

### After adding/modifying methods:
```bash
node packages/core/scripts/generate-client.cjs
```

## Peer Resolution

```ts
import { resolvePeer, resolveUser, resolveChannel } from '../users/resolve-peer.js'

// General peer resolution → tl.TypeInputPeer
const peer = await resolvePeer(client, chatId)

// Type-specific shortcuts
const user = await resolveUser(client, userId)      // → tl.TypeInputUser
const channel = await resolveChannel(client, chatId) // → tl.TypeInputChannel
```

`InputPeerLike` accepts: marked peer ID (number), username (string), `"me"`/`"self"`, TL peer objects, or high-level `User`/`Chat` objects.

### Chat vs Channel dispatch

```ts
import { isInputPeerChannel, toInputChannel } from '../../utils/peer-utils.js'

const peer = await resolvePeer(client, chatId)
if (isInputPeerChannel(peer)) {
  await client.call({ _: 'channels.deleteMessages', channel: toInputChannel(peer), id: ids })
} else {
  await client.call({ _: 'messages.deleteMessages', id: ids, revoke })
}
```

Other peer utilities: `isInputPeerChat`, `isInputPeerUser`, `getMarkedPeerId`, `parseMarkedPeerId`.

For wrong peer type: `throw new MtInvalidPeerTypeError(peerId, 'chat or channel')`.

### Batched queries

```ts
import { _getUsersBatched, _getChatsBatched, _getChannelsBatched } from '../chats/batched-queries.js'

const user = await _getUsersBatched(client, toInputUser(peer))
```

Coalesces multiple individual requests into single `users.getUsers`/`messages.getChats`/`channels.getChannels` calls.

## Update Handling

### Methods returning `tl.TypeUpdates`

```ts
const res = await client.call({ _: 'channels.editAdmin', ... })
client.handleClientUpdate(res)
```

Pass `noDispatch` to suppress update dispatch:
```ts
client.handleClientUpdate(res, true)  // sync PTS only, don't dispatch
```

### Finding messages in update responses

```ts
import { _findMessageInUpdate } from './find-in-update.js'

// For send methods:
const msg = _findMessageInUpdate(client, res, false, !params.shouldDispatch, false, randomId)

// For edit methods (isEdit=true):
const msg = _findMessageInUpdate(client, res, true, !params.shouldDispatch)

// Nullable variant:
const msg = _findMessageInUpdate(client, res, false, !params.shouldDispatch, true)
```

### Methods returning `messages.affectedMessages` / `messages.affectedHistory`

These carry PTS but are not Updates objects. Create a dummy update:

```ts
import { createDummyUpdate } from '../../updates/utils.js'

const res = await client.call({ _: 'channels.deleteMessages', channel, id: ids })
const upd = createDummyUpdate(res.pts, res.ptsCount, peer.channelId)  // channelId for channel-scoped PTS
client.handleClientUpdate(upd)

// For non-channel:
const upd = createDummyUpdate(res.pts, res.ptsCount)
```

### `shouldDispatch` pattern

Methods that return updates typically accept `shouldDispatch?: true` in params. The convention is to NOT dispatch by default (pass `!params.shouldDispatch` to noDispatch):

```ts
client.handleClientUpdate(res, !params.shouldDispatch)
// or for _findMessageInUpdate:
_findMessageInUpdate(client, res, false, !params.shouldDispatch)
```

## Text / Entity Normalization

```ts
import { _normalizeInputText } from '../misc/normalize-text.js'

const [message, entities] = await _normalizeInputText(client, text)
```

`InputText` is `string | { text: string, entities: tl.TypeMessageEntity[] }`. Handles mention entity resolution and whitespace trimming.

## Handling 2FA passwords

Some methods require passing a 2FA password, inside the `InputCheckPasswordSRP` type.
You are expected to add a `password` field to the `params` object, and use it as follows:

```ts
const password = await client.computeSrpParams(
  await client.call({
    _: 'account.getPassword',
  }),
  params.password,
)
```

## Sending Messages

Common send methods use `_processCommonSendParameters` and `_maybeInvokeWithBusinessConnection`:

```ts
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
import { randomLong } from '../../../utils/long-utils.js'

const { peer, replyTo, scheduleDate, chainId, quickReplyShortcut } = await _processCommonSendParameters(client, chatId, params)

const randomId = randomLong()
const res = await _maybeInvokeWithBusinessConnection(
  client,
  params.businessConnectionId,
  {
    _: 'messages.sendMessage',
    peer,
    replyTo,
    randomId,
    scheduleDate,
    message,
    entities,
    silent: params.silent,
    clearDraft: params.clearDraft,
    noforwards: params.forbidForwards,
    sendAs: params.sendAs ? await resolvePeer(client, params.sendAs) : undefined,
    quickReplyShortcut,
    effect: params.effect,
    allowPaidFloodskip: params.allowPaidFloodskip,
    allowPaidStars: params.allowPaidMessages,
  },
  { chainId, abortSignal: params.abortSignal },
)
```

### Chain ID for message ordering

```ts
import { _getPeerChainId } from '../misc/chain-id.js'

const chainId = _getPeerChainId(client, peer, 'send')
await client.call(request, { chainId })
```

## File / Media Normalization

```ts
// InputFileLike → tl.TypeInputFile (triggers upload if needed)
const file = await client._normalizeInputFile(input, params)

// InputMediaLike → tl.TypeInputMedia
const media = await client._normalizeInputMedia(media, params)
```

## Pagination

### Offset-based with `ArrayPaginated`

```ts
import { makeArrayPaginated, ArrayPaginated } from '../../utils/index.js'

// @exported
export interface GetHistoryOffset { id: number; date: number }

export async function getHistory(
  client: ITelegramClient,
  chatId: InputPeerLike,
  params?: { limit?: number; offset?: GetHistoryOffset },
): Promise<ArrayPaginated<Message, GetHistoryOffset>> {
  // ...fetch and parse...
  const last = msgs[msgs.length - 1]
  const next = last ? { id: last.id, date: last.raw.date } : undefined
  return makeArrayPaginated(msgs, res.count ?? msgs.length, next)
}
```

`ArrayPaginated<T, Offset>` extends `Array<T>` with `.next` (next offset or `undefined`) and `.total`.

### Iterator wrapper over paginated getter

```ts
export async function* iterHistory(
  client: ITelegramClient,
  chatId: InputPeerLike,
  params?: Parameters<typeof getHistory>[2] & {
    limit?: number      // default Infinity
    chunkSize?: number  // default 100
  },
): AsyncIterableIterator<Message> {
  const peer = await resolvePeer(client, chatId)  // resolve once
  let { offset } = params ?? {}
  let current = 0

  for (;;) {
    const res = await getHistory(client, peer, {
      offset,
      limit: Math.min(chunkSize, limit - current),
    })
    for (const msg of res) {
      yield msg
      if (++current >= limit) return
    }
    if (!res.next) return
    offset = res.next
  }
}
```

### `ArrayWithTotal` for non-paginated responses with count

```ts
import { makeArrayWithTotal } from '../../utils/index.js'
return makeArrayWithTotal(items, total)
```

## Return Types

Wrap raw TL objects in high-level classes:

```ts
import { Message, PeersIndex } from '../../types/index.js'

// Build peer index from response
const peers = PeersIndex.from(res)

// Single message
return new Message(res.message, peers)

// Array
return res.users.map(u => new User(u))

// Nullable
return res ? new Chat(res) : null

// Filter empties
const msgs = res.messages.filter(m => m._ !== 'messageEmpty').map(m => new Message(m, peers))
```

## Error Handling

```ts
import { MtArgumentError } from '../../../types/errors.js'
import { MtTypeAssertionError } from '../../../types/errors.js'
import { MtInvalidPeerTypeError } from '../../types/errors.js'
import { MtMessageNotFoundError } from '../../types/errors.js'
import { assertTypeIs, assertTypeIsNot } from '../../../utils/type-assertions.js'

// Type assertions on TL responses
assertTypeIsNot('getHistory', res, 'messages.messagesNotModified')
assertTypeIs('getFullUser', res.fullUser, 'userFull')

// Argument validation
throw new MtArgumentError('mustReply used, but replyTo was not passed')

// Peer type mismatch
throw new MtInvalidPeerTypeError(chatId, 'channel')
```

## Common Utilities

```ts
import { randomLong } from '../../../utils/long-utils.js'
import { normalizeDate, normalizeMessageId } from '../../utils/index.js'
import { getMarkedPeerId, parseMarkedPeerId } from '../../../utils/peer-utils.js'
import { inputPeerToPeer } from '../../utils/peer-utils.js'
import { Long } from 'long'
```

- `randomLong()` — random `Long` for `randomId` fields
- `normalizeDate(d)` — `Date | number | undefined` → UNIX seconds
- `normalizeMessageId(m)` — `number | Message | undefined` → `number | undefined`
- `Long.ZERO` — for `hash` fields (from `long` package)

## TL Flag Mapping

Optional boolean TL flags → optional params:
```ts
{
  _: 'messages.sendMessage',
  noWebpage: params.disableWebPreview,    // undefined = flag not set
  silent: params.silent,
  clearDraft: params.clearDraft,
  noforwards: params.forbidForwards,
}
```

Optional peer params with ternary:
```ts
sendAs: params.sendAs ? await resolvePeer(client, params.sendAs) : undefined,
```

Destructure defaults at top:
```ts
const { limit = 100, offset = 0 } = params ?? {}
```

## File Organization

- Public methods: named after the action (`send-text.ts`, `get-history.ts`, `delete-messages.ts`)
- Internal helpers: prefixed `_`, annotated `@internal @noemit` (`_processCommonSendParameters`, `_findMessageInUpdate`)
- Shared params interfaces: exported with `// @exported` from the relevant file
- Normalizers: prefixed `_normalize*` (`_normalizeInputText`, `_normalizeInputMedia`)
- Utility files: `_utils.ts` or `_business-connection.ts` etc.
- Tests: colocated as `*.test.ts`

Related Skills

update-layer

451
from mtcute/mtcute

Use when updating TL schema layer in mtcute, fetching new Telegram API schemas, or when user asks to update the layer/schema

using-mtcute

451
from mtcute/mtcute

Use when working with mtcute, @mtcute/* packages, Telegram MTProto in TypeScript, TL types, Telegram API methods, or building Telegram bots/clients in TypeScript. Not for Bot API wrapper libraries (grammy, telegraf, node-telegram-bot-api).

article-writing

144923
from affaan-m/everything-claude-code

Write articles, guides, blog posts, tutorials, newsletter issues, and other long-form content in a distinctive voice derived from supplied examples or brand guidance. Use when the user wants polished written content longer than a paragraph, especially when voice consistency, structure, and credibility matter.

Content Creation & MarketingClaude

copywriting

31392
from sickn33/antigravity-awesome-skills

Write rigorous, conversion-focused marketing copy for landing pages and emails. Enforces brief confirmation and strict no-fabrication rules.

MarketingClaude

blog-writing-guide

31392
from sickn33/antigravity-awesome-skills

This skill enforces Sentry's blog writing standards across every post — whether you're helping an engineer write their first blog post or a marketer draft a product announcement.

Writing AssistantClaude

afrexai-copywriting-mastery

3891
from openclaw/skills

Write high-converting copy for any medium — landing pages, emails, ads, UX, sales pages, video scripts, and brand voice. Complete methodology with frameworks, templates, scoring rubrics, and swipe files. Use when writing or reviewing any user-facing text.

Content & Documentation

afrexai-conversion-copywriting

3891
from openclaw/skills

Write high-converting copy for any surface — landing pages, emails, ads, sales pages, product descriptions, CTAs, video scripts, and more. Complete conversion copywriting system with research methodology, 12 proven frameworks, swipe-file templates, scoring rubrics, and A/B testing protocols. Use when you need to write or review any copy meant to drive action.

Content & Documentation

human-writing

3891
from openclaw/skills

Guidelines and standards for professional, human-like writing and documentation. Use this skill when generating READMEs, technical documentation, code comments, or any formal written output to avoid common AI 'tells', buzzwords, and stylistic tropes. Ensure content follows the 'Professional Human in the Field' standard: high precision, zero fluff, and no emojis in technical contexts.

Content & Documentation

marketing-copywriting

3891
from openclaw/skills

Generate marketing copy, emails, and promotional content based on customer personas with multi-style rewriting capabilities

Content & Documentation

writing-anti-ai

167
from cnfjlhj/ai-collab-playbook

This skill should be used when the user asks to "remove AI writing patterns", "humanize this text", "make this sound more natural", "remove AI-generated traces", "fix robotic writing", or needs to eliminate AI writing patterns from prose. Supports both English and Chinese text. Based on Wikipedia's "Signs of AI writing" guide, detects and fixes inflated symbolism, promotional language, superficial -ing analyses, vague attributions, AI vocabulary, negative parallelisms, and excessive conjunctive phrases.

Content & Documentation

writing-content

6
from ai-mindset-org/pos-sprint

An interactive content creation skill inspired by the Julian Shapiro framework, featuring research, scoring, and AI-slop detection.

Content & DocumentationClaude

writing-spec

44152
from streamlit/streamlit

Writes product and tech specs for new Streamlit features. Use when designing new API commands, widgets, or significant changes that need team review before implementation.