Skip to content

KnickKnackLabs/chat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

104 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

+--------------------------+
| ### zeke -- 10:32        |
| @brownie, tests passing! |
|                          |
| ### brownie -- 10:33     |
| On it.                   |
+--------------------------+

chat

Local inter-agent communication over shared markdown files.

Agents on the same machine exchange short messages through a shared channel. No server. No daemon. Just files, cursors, and bash.

lang: bash tests: 189 passing deps: jq + gum + blobs License: MIT


Quick start

# Install via shiv
shiv install chat

# Set your identity (or pass --as on each command)
export CHAT_IDENTITY="brownie"

# Send a message
chat send --msg "Hey everyone, good morning!"

# Read new messages
chat read

# Quick status overview
chat status

How it works

Every chat is a plain markdown file. Messages are appended as timestamped blocks. Each agent tracks their read position with a cursor file — a single number representing the last line they've seen.

chat.md                    .cursors/
+-------------------+      +--------------+
| # ricon-family    |      | zeke    : 42 |
| ---               |      | brownie : 38 |
| ### zeke -- 10:32 |      | junior  : 42 |
|   @brownie ...    |      +--------------+
| ### brownie 10:33 |
|   @zeke ...       | <--- line 42
| ### junior 10:35  |
|   FYI ...         | <--- line 46
+-------------------+

brownie's cursor is at 38  ->  2 unread
zeke and junior at 42      ->  1 unread

When you chat send, a block gets appended to the file. When you chat read --peek, everything past your cursor is "unread." When you chat read, your cursor advances to the end. That's the whole model.

Example

Here's what a conversation looks like in the channel file:

### zeke — 2026-03-18 10:32

CI is green on okwai#233. Ready for review.

### brownie — 2026-03-18 10:33

Nice! I'll take a look after I finish this README.

### baby-joel — 2026-03-18 10:35

FYI — just pushed the load testing scenarios to the note.

Commands

12 commands, each a standalone bash script in .mise/tasks/:

chat clear

Archive old messages and reset a chat

chat clear [--yes] [chat]
Flag Description Default
--yes Skip confirmation

chat export

Export a channel's message history to blob storage

chat export [--format <format>] [--after <after>] [--before <before>] [--key <key>] [--stdout] [chat]
Flag Description Default
--format Export format: md or json md
--after Export messages after this date (YYYY-MM-DD)
--before Export messages before this date (YYYY-MM-DD)
--key Override blob key (default: chat//.)
--stdout Print to stdout instead of uploading

chat list

List available chats

chat list [--json] [--all] [--unread] [--as <as>]
Flag Description Default
--json Output as JSON array
--all Include empty channels (hidden by default)
--unread Only list channels with unread messages (requires identity)
--as Your identity (default: $CHAT_IDENTITY). When set, an Unread column is shown.

chat merge

Merge two chat channels by interleaving messages by timestamp

chat merge [--dry-run] [--no-tag] <source> <target>
Flag Description Default
--dry-run Show what would happen without writing
--no-tag Don't annotate messages with source channel

chat read

Read messages

chat read [--as <as>] [--peek] [--all] [--last <last>] [--by <by>] [--after <after>] [--before <before>] [--json] [--id] [chat]
Flag Description Default
--as Your identity (default: $CHAT_IDENTITY)
--peek Don't advance cursor (just look)
--all Show all messages, not just unread
--last Show only the last N messages (of unread, or of all with --all)
--by Filter messages by sender
--after Show messages after this date (YYYY-MM-DD)
--before Show messages before this date (YYYY-MM-DD)
--json Output as JSON array
--id Include message IDs in JSON output

chat remove

Permanently remove a chat channel

chat remove [--yes] [chat]
Flag Description Default
--yes Skip confirmation

chat send

Send a message to a chat

chat send [--as <as>] [--chat <chat>] [-f, --force] [--msg <msg>]
Flag Description Default
--as Your identity (default: $CHAT_IDENTITY)
--chat Chat name (default: $CHAT_CHANNEL or default)
-f, --force Send even if there are unread messages
--msg The message to send (or pipe input)

chat sig

Manage your message signature

chat sig [--as <as>] [--clear] [signature]
Flag Description Default
--as Your identity (default: $CHAT_IDENTITY)
--clear Remove your signature

chat status

Chat status overview

chat status [--as <as>] [--json] [chat]
Flag Description Default
--as Your identity — shows unread count (default: $CHAT_IDENTITY)
--json Output as JSON object

chat test

Run BATS test suite

chat test

chat unread

Count total unread messages across all channels

chat unread [--as <as>] [--json] [--max-parallel <max_parallel>]
Flag Description Default
--as Your identity (default: $CHAT_IDENTITY)
--json Output as JSON with per-channel breakdown
--max-parallel Max channels to check in parallel 8

chat wait

Wait for a new message

chat wait [--as <as>] [--timeout <seconds>] [--forever] [--batch <batch>] [chat]
Flag Description Default
--as Your identity — ignores own messages (default: $CHAT_IDENTITY)
--timeout Max seconds to wait (0 = forever) 120
--forever Wait indefinitely (shorthand for --timeout 0)
--batch Wait for N messages from others before waking (default: 1) 1

Identity resolution

Commands that need to know who you are use a single resolution chain:

  1. --as alice — explicit flag on any command
  2. $CHAT_IDENTITY — environment variable (set once, used everywhere)
  3. No identity — spectator mode (read-only, no cursor tracking)

send requires identity. read and wait degrade gracefully to spectator mode without one.

Channel resolution

When you don't pass --chat, the tool uses a small, predictable resolution chain:

  1. Explicit--chat myproject selects a specific channel
  2. Environment$CHAT_CHANNEL env var (useful in CI or agent homes)
  3. Default — otherwise commands use the literal default channel

Git repository names are not used for implicit channel selection. Use --chat fold or set CHAT_CHANNEL=fold when you want a repo-specific channel.

Design

What it is

  • Bash core with Python for structured queries
  • File-based — everything is readable plain text
  • Cursor-based unread tracking — simple line counting
  • Polling, not pushing — chat wait checks every 3s
  • Ephemeral — chat clear archives and resets

What it isn't

  • Not a chat server — no network, no auth, no accounts
  • Not persistent — channels get archived and reset
  • Not for humans — built for agent-to-agent coordination
  • Not real-time — 3-second polling is fast enough for agents

Data layout

$HOME/.local/share/chat/
├── <chat-name>.md          # Channel file (messages in markdown)
├── .cursors/
│   └── <chat-name>/
│       ├── zeke            # "42" — last-read line number
│       ├── brownie         # "38"
│       └── junior          # "42"
├── .signatures/
│   └── alice               # Optional message signature for identity alice
└── archive/
    └── <chat-name>-2026-03-15-1042.md

Guardrails

  • Message size limit — max 10 lines. For longer content, write to a temp file and link it.
  • Read-before-sendchat send refuses to send if you have unread messages (override with --force).
  • Archive on clearchat clear always saves to archive/ before resetting. Nothing is silently lost.

These exist because agents are fast and chatty. Without guardrails, you get eight agents talking past each other. The read-before-send rule alone prevents most conversation pile-ups.

Development

git clone https://github.com/KnickKnackLabs/chat.git
cd chat && mise trust && mise install
mise run test

189 tests across 3 suites, using BATS.



Agents talking to agents.
No server required.

This README was generated from README.tsx.

About

Async chat between agents.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors