Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
__pycache__
.cf_session*
*.Chatfile
Chatfile
11 changes: 8 additions & 3 deletions SKILL.md → AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ Minimal agent collaboration via shared text files. No HTTP, no dependencies.

## The `cf` Tool

Single command for all chatfile operations. State stored in `.cf_session` (no env vars needed).
Single command for all chatfile operations. State is dynamically isolated per terminal or process (e.g., `.cf_session.pts_8` or `.cf_session.sid_1234`) to prevent agent overlap (no env vars needed).

### Room Management

```bash
# Create a new room (append-only)
cf create-room myproject # Creates myproject.Chatfile
cf create-room # Creates Chatfile
cf create-room -g myproject # Creates global room in ~/.chatfiles/

# List available rooms
# List available rooms (local and global)
cf list-rooms

# Register with a room (get unique name)
# Delete a room (handles +a attr removal)
cf delete-room myproject.Chatfile

# Register with a room (get unique name, searches local & global)
cf register myproject.Chatfile
cf register myproject # Auto-resolves to myproject.Chatfile
# Output: swift-raven-1234

# Join the room (announces entry)
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
1 change: 1 addition & 0 deletions GEMINI.md
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@ A bash tool for managing chatrooms via Chatfiles.
### Installation

```bash
# Install to ~/.local/bin
./install.sh

# Or manually
chmod +x cf
# Optionally add to PATH or create alias
# Add to PATH or create alias
```

### Commands

**Room Management**
- `cf create-room [name]` - Create a room (`name.Chatfile` or `Chatfile`), attempts to set append-only
- `cf list-rooms` - List available rooms in current directory
- `cf register <chatfile>` - Register with a chatfile (generates a unique name like `swift-fox-1234`)
- `cf create-room [name]` - Create a local room (`name.Chatfile` or `Chatfile`), attempts to set append-only
- `cf create-room -g name` - Create a global room in `~/.chatfiles/`
- `cf list-rooms` - List local and global rooms
- `cf delete-room <file>` - Delete a room (handles `+a` attr removal via sudo)
- `cf register <chatfile>` - Register with a chatfile (searches local & `~/.chatfiles/`, auto-resolves `.Chatfile` extension)
- `cf join` - Join the room (announces entry)
- `cf leave` - Leave the room (announces exit)

Expand Down Expand Up @@ -58,4 +64,4 @@ cf await
cf leave
```

Session state is stored in `.cf_session` in the current directory.
Session state is dynamically isolated per terminal or process (e.g., `.cf_session.pts_8` or `.cf_session.sid_1234`) in the current directory to prevent agent overlap. Global rooms are stored in `~/.chatfiles/`.
103 changes: 86 additions & 17 deletions cf
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,38 @@ GLOBAL_DIR="$HOME/.chatfiles"
CMD="${1:-help}"
shift 2>/dev/null || true

get_session_id() {
# 1. Use controlling TTY if available (stable across invocations)
local tty
tty=$(ps -o tty= -p $$ 2>/dev/null | tr -d ' \n')
if [ -n "$tty" ] && [ "$tty" != "?" ]; then
echo "${tty//\//_}"
return
fi
# 2. Use PPID (parent shell PID) — stable for all commands from the same shell
if [ -n "$PPID" ] && [ "$PPID" != "1" ]; then
echo "ppid_${PPID}"
return
fi
# 3. Last resort: own SID (may vary per invocation in detached contexts)
local sid
sid=$(ps -o sid= -p $$ 2>/dev/null | tr -d ' \n')
if [ -n "$sid" ]; then
echo "sid_${sid}"
return
fi
echo "$$"
}

SESSION_FILE="${CF_SESSION_FILE:-.cf_session.$(get_session_id)}"

find_session() {
if [ -f ".cf_session" ]; then
if [ -f "$SESSION_FILE" ]; then
echo "$SESSION_FILE"
elif [ -f ".cf_session" ]; then
echo ".cf_session"
elif [ -f "$HOME/$SESSION_FILE" ]; then
echo "$HOME/$SESSION_FILE"
elif [ -f "$HOME/.cf_session" ]; then
echo "$HOME/.cf_session"
else
Expand All @@ -36,12 +65,14 @@ load_session() {
read -r CHATFILE < "$sf"
read -r MYNAME < <(sed -n '2p' "$sf")
JOINED=$(sed -n '3p' "$sf")
LASTLINE=$(sed -n '4p' "$sf")
LASTLINE="${LASTLINE:-0}"
}

save_session() {
local sf
sf=$(find_session) || sf=".cf_session"
printf '%s\n%s\n%s\n' "$CHATFILE" "$MYNAME" "$JOINED" > "$sf"
sf=$(find_session) || sf="$SESSION_FILE"
printf '%s\n%s\n%s\n%s\n' "$CHATFILE" "$MYNAME" "$JOINED" "$LASTLINE" > "$sf"
}

case "$CMD" in
Expand Down Expand Up @@ -183,21 +214,31 @@ case "$CMD" in
noun="${NOUNS[RANDOM % ${#NOUNS[@]}]}"
suffix=$((RANDOM % 9000 + 1000))
MYNAME="${adj}-${noun}-${suffix}"
grep -q "^${MYNAME}:" "$CHATFILE" 2>/dev/null || break
((attempts++))
# Check both message format (name:) and join/leave format ([name ...])
if ! grep -q -e "^${MYNAME}:" -e "\\[${MYNAME} " "$CHATFILE" 2>/dev/null; then
break
fi
((attempts++)) || true
done

if [ $attempts -ge 100 ]; then
echo "Failed to generate unique name after 100 attempts" >&2
exit 1
fi

JOINED=""
printf '%s\n%s\n%s\n' "$CHATFILE" "$MYNAME" "$JOINED" > .cf_session
LASTLINE=0
printf '%s\n%s\n%s\n%s\n' "$CHATFILE" "$MYNAME" "$JOINED" "$LASTLINE" > "$SESSION_FILE"
echo "$MYNAME"
;;

join|j)
load_session
[ -n "$JOINED" ] && { echo "Already joined as $MYNAME" >&2; exit 1; }
JOINED="yes"
save_session
printf '[%s joined]\n' "$MYNAME" >> "$CHATFILE"
LASTLINE=$(wc -l < "$CHATFILE")
save_session
echo "Joined as $MYNAME"
;;

Expand All @@ -214,26 +255,54 @@ case "$CMD" in
[ -z "$1" ] && { echo "Usage: cf send \"message\"" >&2; exit 1; }
load_session
[ -z "$JOINED" ] && { echo "Must join first: cf join" >&2; exit 1; }
printf '%s: %s\n' "$MYNAME" "$1" >> "$CHATFILE"
# Strip newlines to preserve one-message-one-line invariant
msg="${1//$'\n'/ }"
printf '%s: %s\n' "$MYNAME" "$msg" >> "$CHATFILE"
;;

await|a|wait|w)
load_session
[ -z "$JOINED" ] && { echo "Must join first: cf join" >&2; exit 1; }
LAST=$(tail -n 1 "$CHATFILE" | cut -d: -f1)
if [ "$LAST" = "$MYNAME" ]; then
tail -f -n 0 "$CHATFILE" | head -n 1
else
tail -n 1 "$CHATFILE"
fi
exec 3< <(tail -n +$((LASTLINE + 1)) -f "$CHATFILE")
TAILPID=$!
trap 'kill $TAILPID 2>/dev/null' EXIT
while IFS= read -r line <&3; do
((LASTLINE++))
case "$line" in
\[*) save_session; continue ;;
"${MYNAME}:"*) save_session; continue ;;
esac
save_session
echo "$line"
break
done
kill $TAILPID 2>/dev/null
exec 3<&-
trap - EXIT
;;

send-await|sa)
[ -z "$1" ] && { echo "Usage: cf send-await \"message\"" >&2; exit 1; }
load_session
[ -z "$JOINED" ] && { echo "Must join first: cf join" >&2; exit 1; }
printf '%s: %s\n' "$MYNAME" "$1" >> "$CHATFILE"
tail -f -n 0 "$CHATFILE" | head -n 1
msg="${1//$'\n'/ }"
printf '%s: %s\n' "$MYNAME" "$msg" >> "$CHATFILE"
exec 3< <(tail -n +$((LASTLINE + 1)) -f "$CHATFILE")
TAILPID=$!
trap 'kill $TAILPID 2>/dev/null' EXIT
while IFS= read -r line <&3; do
((LASTLINE++))
case "$line" in
\[*) save_session; continue ;;
"${MYNAME}:"*) save_session; continue ;;
esac
save_session
echo "$line"
break
done
kill $TAILPID 2>/dev/null
exec 3<&-
trap - EXIT
;;

read|cat)
Expand Down Expand Up @@ -277,7 +346,7 @@ Messaging:
Info:
cf status Show current session

State stored in .cf_session (current directory)
State stored in .cf_session.<tty|ppid> or .cf_session (current directory)
Global rooms stored in ~/.chatfiles/
EOF
;;
Expand Down
Loading