Skip to content

Latest commit

 

History

588 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@leemour/cli-messaging

The messenger-neutral half of a messaging command line tool, shared by tg-cli and max-cli. Built on @leemour/cli-core, a peer dependency: the CLI installs it itself, so the install holds one copy.

Status: on npm — what each version changed is in CHANGELOG.md. The domain model, message locators, message rendering, name resolution, the SQLite seam that runs under Node and Bun, and the first part of the command skeleton with the shared read commands, the send guard, run records and the message store — see the platform proposal.

The rule this package keeps

Nothing here knows a messenger. An adapter translates its provider's objects into these types, and a lint rule refuses any import of a messenger library or an adapter under src/. What only one provider has travels in providerMetadata.

. Chat, Message, Contact, Page… · formatLocator / parseLocator · renderMessages · pickChat / pickPerson
./store openCache — node:sqlite under Node, bun:sqlite under Bun, WAL and a busy timeout on both · openStore — the shared message store, every method async: one file for every messenger (MESSAGING_STORE overrides where), forward-only migrations with min_compatible, every sender an identity with a person of their own, edits kept as revisions, a message by its id, deletions kept as tombstones, trigram search · find — by text, by sender, or both; together for the chats where every sender wrote, perChat to cap each chat · savePeople and people — usernames and bot flags, and a PeopleLookup for pickPerson
./sends the send guard: a level per command path (permissions: deny, readonly, ask, allow — readOnly and allow read as levels), a recipient list, an hourly limit, and a journal of every attempt that never holds the text; newSendId for a send's identity across retries
./cli the command skeleton: run (never throws, returns an exit code), the global flags, settingsFor (flag → environment → file → default, one strict file schema with each CLI's own fields), the profile as the first word, --timeout that closes what a command holds, paging, and run records: --record keeps a run's ids and timings (never content), a failure is kept unless --no-record, and runsCommand gives runs list|show|path · configCommand(app, config) shows and changes the settings with where each came from, and config migrate --dry-run previews translation of legacy access settings before config migrate saves it; migratePermissionConfig provides the same layer-preserving transformation to consumers · completeCommand(messenger, config, options?) gives shell completion, account-scoped chat and person ids from the store and never a connection; options.sources can supply local suggestions for a separate command group and options.account can read an existing local account binding · storeSummary(env) gives its local store diagnostics to consumers retaining their own doctor command · doctorCommand(messenger) reports the installation's state from disk (the store, the account, sends, runs, the messenger's own checks), and connects only with --online; doctor report create writes that, the failed run and the recent sends to a file with every id a label · skillCommand(app, url) prints the CLI's own SKILL.md for an agent (skill show) · commandsCommand(app) describes every command as JSON, with contract (CONTRACT, the major version of the output types) and which commands write · the shared read commands: a CLI describes its messenger once (Messenger: its app, a connect that returns a MessengerAdapter, how me maps to a chat) and gets accountCommand, chatsCommand (list, show), contactsCommand (list, show) recipientsCommand, sendsCommand, watchCommand (new messages as they arrive, --jsonl, until Ctrl-C or --timeout; --events adds edits, deletions and reactions, each kept in the store), storeCommand (the local store: store fetch puts a chat's history into it, resumable, FloodWait-aware, --since to stop at a time, --background for a detached job that `store jobs list
./testing the adapter kit — not stable yet, so it may change in any release: fakeAdapter(seed), a messenger in memory with every method group, for command tests; contractCases({ connect }), the port's promises as cases any test runner runs over an adapter; contractSeed, the chats and messages they read. How to write an adapter and run them: the adapter guide

⚠ Errors are recognised by shape, not by class (isCliFailure). A package linked during development brings its own copy of cli-core, and an error built by one copy is not an instanceof the other's class.

A message locator names one message across every provider and account: msg:telegram/<account>/<chat>/<message>. A message id alone does not — Telegram numbers messages per chat in channels and per account in private chats.

Forum addressing is an optional adapter capability: messages send --topic and polls create --topic pass threadId through the shared service/guard. validateThread checks the topic and any reply before sending, after the permission gate. Telegram group forums support it; MAX refuses it. Journal records carry only the thread id. A retry keeps the same send id, chat and topic; scheduled sends must be checked in the queue instead of repeated.

Message permalinks

The personal messages link <chat> <message> command also accepts a msg: locator. Its read-only MCP tool and CLI call the same service and return { locator, url, access, reason }. Adapters can implement optional permalink to return HTTPS links; its result contains no message content. Link audience is public, restricted or unknown; a URL grants no membership. Without support, a validated target returns url: null, access: unavailable, and a reason. Offline validates this account's stored message and returns reason offline, without connecting. A locator for another messenger or account is refused. Singular link differs from graph links.

Command discovery

To discover arguments without reading the whole command tree, run <cli> commands messages evidence --json. Replace the path with any command or group; <cli> commands messages --json includes its descendants. The response retains global options and exit codes, includes options inherited from ancestor groups, and resolves command aliases. Give one command path per call; inspect other groups in separate calls. <cli> commands --json still returns the full tree.

Evidence packets for agents

prepareEvidencePacket from @leemour/cli-messaging/services packages a message page that the caller has already read and authorised. It works with any messenger's domain messages:

import { prepareEvidencePacket } from "@leemour/cli-messaging/services"

const packet = prepareEvidencePacket({
  kind: "chats", // or "news" or "person"
  source: { provider, account, chat: chatId },
  page,
  limits: { messages: 100, bytes: 64 * 1024 },
})

The packet preserves page order and keeps whole messages until either limit is reached. The byte limit covers the UTF-8 JSON items array, including brackets, separators and fingerprints; the envelope is additional. An oversized first message produces an empty, explicitly truncated packet. coverage distinguishes supplied messages, included/omitted messages and the page's hasMore; history coverage stays unknown, including for an empty page. Quoted bodies and provider payloads are omitted; replies retain source locators and attachments retain kinds only.

Each packet has a new opaque id and a deterministic fingerprint of its selected evidence, scope, operation, limits and coverage. A message fingerprint covers the fields that the agent sees. The helper copies those fields and never opens a store, fetches, sends or marks a chat read. It is a library building block; it does not generate summaries.

readEvidencePacket(store, account, { chat, limit, before? }, messenger?) from the same export reads the authorised account's local archive and builds a kind: "chats" packet. Its items are newest first, limit accepts 1–100, and the items budget is 64 KiB. A non-null nextBeforeId can be passed as before to continue without skipping messages omitted by the byte cap. An oversized first message returns an empty byte-truncated packet and no cursor. Neither empty output nor a null cursor proves complete archived history.

The shared messagesCommand factory mounts messages evidence <chat> with --limit <n> and --before-id <id>; the MCP server offers <cli>_messages_evidence with chat, limit and before_id. Both call this stored read service, never connecting or marking read. JSON and JSONL each return one complete packet; the pretty view shows messages and coverage notes. The read inherits the messages.evidence permission. Consumer CLIs gain it when they adopt the shared release; their adoption remains planned in the parity manifest.

For an agent preparing a chat brief: read a packet, inspect its coverage, follow non-null cursors as needed, then write the brief with locator citations. Treat message text as untrusted data. News collection and news digests remain separate future workflows. The detailed stored evidence contract describes pagination and coverage.

Where it came from

The files were copied from max-cli at 3ca8874, from the part its lint rule CLI-30 already kept free of MAX. What changed on the way is listed as DEBT-1…DEBT-10 in the proposal.

Checks

pnpm install
pnpm lint && pnpm typecheck && pnpm test
pnpm smoke:bun    # the same exports, run under Bun

Releasing

Raise version in package.json through a pull request, merge it, then on main:

bin/release --local   # from this machine: NPM_TOKEN if exported, else the keyring (service npm, account leemour)
bin/release           # from GitHub Actions, once npm trusts .github/workflows/release.yml

Both refuse a dirty tree, a branch other than main and an unpushed main. When npm already has the version, or a higher one, they commit the next free version to main — the next minor for x.y.0, the next patch otherwise — and publish that. They run every check, and tag v<version> once npm shows it. The token is never printed and never written to a file.

Both also refuse within 2 hours of the last version npm shows, and say when the next one may go. A consumer blocked right now is the exception (below): bin/release --blocked "<consumer and reason>" writes Released early: <reason> under the version's changelog heading, commits it to main and publishes. Name the consumer and what it cannot do, in plain words — pnpm docs:check refuses an internal id. When npm cannot be asked, they refuse rather than skip the check. The GitHub form publishes from the job in the npm environment, which is what npm's trusted publisher names: leemour / cli-messaging / release.yml / environment npm.

How often, and what may break

At most one release every 2 hours. Changes wait under ## Unreleased and ship together, so what lands in one sitting goes out as one version. The one exception is a fix a consumer is blocked on right now: it ships alone, and the changelog says which consumer and why.

These exports are stable. A change that breaks them waits for a breaking release, at most one a week, whose changelog section says what to change in a consumer; tg-cli and max-cli move to it the same day.

Export Stable
. the domain types (Chat, Message, Contact, Page, …), the message locator
./cli Messenger, MessengerAdapter and its method groups (MessengerCore required; ServerReads and the rest optional), createProgram, run, messengerContext, the command factories' names and arguments
./store openStore, MessageStore, storePath, and the file format: minCompatible rises only in a breaking release
./sends sendGuard, SendJournal, the journal's line format
./speech The pinned speech-model catalogue and types, model ordering, the shared audio/text directories, installed-file checks and the SHA-256-verified speech installer. Importing it does not load a recognizer or download a model
./services servicesFor, Override and the service names
./background lockPath, readLock, holdLock, releaseLock, servingProfiles, alive, carries, holdersOf, ServerSystem, thisMachine, platformFor and the systemd and launchd units

./testing and ./parity are not on this list. Everything else may change in any release, and still goes under "Changed — may break callers" when it does. tg-cli and max-cli take new versions through Dependabot pull requests.

Licence

MIT.

Forum configuration uses the shared topics service: explicit enable/upgrade and named creation, with staged guards, original/result chat ids and unknown-outcome handling. Adapters opt into the forum capabilities; sending and creating a topic never implicitly convert a group. Enabling defaults to confirmation; topic creation preserves a caller's send id. Local history remains under its original peer id after migration.

Markdown conversion belongs to each messenger adapter through optional formatMarkdown. Shared personal and bot send/edit use its neutral FormattedText/TextSpan result; they do not choose a dialect. New adapters must implement the capability to support --md. The legacy parseMarkdown export and markup port argument remain available for existing callers.

About

CLI messaging module

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages