docs: add docs:list helper and front matter

This commit is contained in:
Peter Steinberger
2025-12-09 17:51:05 +00:00
parent b3d4e5cfdf
commit bc3a14cde2
44 changed files with 436 additions and 74 deletions

View File

@@ -1,7 +1,13 @@
---
summary: "Default Clawdis agent instructions and tool roster for the personal assistant setup"
read_when:
- Starting a new Clawdis agent session
- Enabling or auditing default tools
---
# AGENTS.md — Clawdis Personal Assistant (default)
## What Clawdis Does
- Runs WhatsApp relay + Pi/Tau coding agent so the assistant can read/write chats, fetch context, and run tools via the host Mac.
- Runs WhatsApp gateway + Pi/Tau coding agent so the assistant can read/write chats, fetch context, and run tools via the host Mac.
- macOS app manages permissions (screen recording, notifications, microphone) and exposes a CLI helper `clawdis-mac` for scripts.
- Sessions are per-sender; heartbeats keep background tasks alive.

View File

@@ -1,3 +1,10 @@
---
summary: "Step-by-step npm release checklist for the Clawdis CLI"
read_when:
- Cutting a new npm release
- Verifying metadata before publishing
---
# Release Checklist (npm)
Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tagging/publishing.
@@ -20,7 +27,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
- [ ] `pnpm lint`
- [ ] `pnpm test` (or `pnpm test:coverage` if you need coverage output)
- [ ] `pnpm run build` (last sanity check after tests)
- [ ] (Optional) Spot-check the web relay if your changes affect send/receive paths.
- [ ] (Optional) Spot-check the web gateway if your changes affect send/receive paths.
5) **Publish**
- [ ] Confirm git status is clean; commit and push as needed.

View File

@@ -1,10 +1,15 @@
---
summary: "Design notes for a direct `clawdis agent` CLI subcommand without WhatsApp delivery"
read_when:
- Adding or modifying the agent CLI entrypoint
---
# Plan: `clawdis agent` (direct-to-agent invocation)
Goal: Add a CLI subcommand that talks directly to the configured agent command (no WhatsApp send), while reusing the same session handling and config clawdis already uses for auto-replies.
## Why
- Sometimes we want to poke the agent directly (same prompt templates/sessions) without sending a WhatsApp message.
- Current flows (`send`, relay, directives) always route through WhatsApp or add wrapping text; we need a clean “talk to agent now” tool.
- Current flows (`send`, gateway, directives) always route through WhatsApp or add wrapping text; we need a clean “talk to agent now” tool.
## Behavior
- Command: `clawdis agent`

View File

@@ -1,3 +1,8 @@
---
summary: "Current agent integration: Pi/Tau as the sole coding agent with config examples"
read_when:
- Changing agent invocation or defaults
---
# Agent Integration 🤖
CLAWDIS now ships with a single coding agent: Pi (the Tau CLI). Legacy Claude/Codex/Gemini/Opencode paths have been removed.

View File

@@ -1,3 +1,8 @@
---
summary: "Target WebSocket gateway architecture, components, and client flows"
read_when:
- Working on gateway protocol, clients, or transports
---
# Gateway Architecture (target state)
Last updated: 2025-12-09

View File

@@ -1,3 +1,8 @@
---
summary: "How inbound audio/voice notes are downloaded, transcribed, and injected into replies"
read_when:
- Changing audio transcription or media handling
---
# Audio / Voice Notes — 2025-12-05
## What works

View File

@@ -1,3 +1,9 @@
---
summary: "End-to-end guide for running Clawdis as a personal assistant with safety cautions"
read_when:
- Onboarding a new assistant instance
- Reviewing safety/permission implications
---
# Building Your Own AI Personal Assistant with clawdis
> **TL;DR:** CLAWDIS (Pi/Tau only) lets you run a proactive assistant over WhatsApp. It can check in on you, remember context across conversations, run commands on your Mac, and even wake you up with music. This doc was originally written for Claude Code; where you see `claude ...`, use `pi --mode rpc ...` instead. A Pi-specific rewrite is coming soon.
@@ -205,7 +211,7 @@ clawdis heartbeat --provider web --to +1234567890 --verbose
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ WhatsApp │────▶│ clawdis │────▶│ Claude │────▶│ Your Mac │
│ (phone) │◀────│ relay │◀────│ CLI │◀────│ (commands) │
│ (phone) │◀────│ gateway │◀────│ CLI │◀────│ (commands) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
```
@@ -234,17 +240,17 @@ Inbound images/audio/video are downloaded and available as `{{MediaPath}}`. Voic
### Sending Media
Include `MEDIA:/path/to/file.png` in Claude's output to attach images. clawdis handles resizing and format conversion automatically.
## Starting the Relay
## Starting the Gateway
```sh
# Foreground (see all logs)
clawdis relay --provider web --verbose
clawdis gateway --provider web --verbose
# With immediate heartbeat on startup
clawdis relay --heartbeat-now
clawdis gateway --heartbeat-now
```
For backgrounding, run the relay under your preferred supervisor (e.g., launchd/systemd) and point it at the same `clawdis relay --provider web --verbose` command.
For backgrounding, run the gateway under your preferred supervisor (e.g., launchd/systemd) and point it at the same `clawdis gateway --provider web --verbose` command.
## Tips for a Great Personal Assistant

View File

@@ -1,3 +1,9 @@
---
summary: "Spec for the Clawdis macOS companion menu bar app and XPC broker"
read_when:
- Implementing macOS app features
- Touching XPC/CLI bridging
---
# Clawdis macOS Companion (menu bar + XPC broker)
Author: steipete · Status: draft spec · Date: 2025-12-05

View File

@@ -1,3 +1,8 @@
---
summary: "All configuration options for ~/.clawdis/clawdis.json with examples"
read_when:
- Adding or modifying config fields
---
# Configuration 🔧
CLAWDIS uses a JSON configuration file at `~/.clawdis/clawdis.json`.
@@ -147,7 +152,7 @@ export CLAWDIS_LOG_LEVEL=debug
export CLAWDIS_CONFIG_PATH=~/.clawdis/clawdis.json
```
## Migrating from Warelay
## Migrating from Clawdis
If you're upgrading from the old `clawdis` name:

View File

@@ -1,3 +1,8 @@
---
summary: "Deprecated newline-delimited control channel API (pre-gateway)"
read_when:
- Maintaining legacy control channel support
---
# Control channel API (newline-delimited JSON)
**Deprecated:** superseded by the WebSocket Gateway protocol (`clawdis gateway`, see `docs/architecture.md` and `docs/gateway.md`). Use only for legacy builds predating the Gateway rollout.
@@ -8,13 +13,13 @@ Endpoint: `127.0.0.1:18789` (TCP, localhost only). Clients reach it via SSH port
Each line is a JSON object. Two shapes exist:
- **Request**: `{ "type": "request", "id": "<uuid>", "method": "health" | "status" | "last-heartbeat" | "set-heartbeats" | "ping", "params"?: { ... } }`
- **Response**: `{ "type": "response", "id": "<same id>", "ok": true, "payload"?: { ... } }` or `{ "type": "response", "id": "<same id>", "ok": false, "error": "message" }`
- **Event**: `{ "type": "event", "event": "heartbeat" | "relay-status" | "log", "payload": { ... } }`
- **Event**: `{ "type": "event", "event": "heartbeat" | "gateway-status" | "log", "payload": { ... } }`
## Methods
- `ping`: sanity check. Payload: `{ pong: true, ts }`.
- `health`: returns the relay health snapshot (same shape as `clawdis health --json`).
- `health`: returns the gateway health snapshot (same shape as `clawdis health --json`).
- `status`: shorter summary (linked/authAge/heartbeatSeconds, session counts).
- `last-heartbeat`: returns the most recent heartbeat event the relay has seen.
- `last-heartbeat`: returns the most recent heartbeat event the gateway has seen.
- `set-heartbeats { enabled: boolean }`: toggle heartbeat scheduling.
## Events
@@ -30,7 +35,7 @@ Each line is a JSON object. Two shapes exist:
"reason": "<error text>" // only on failed/skipped
}
```
- `relay-status` payload: `{ "state": "starting" | "running" | "restarting" | "failed" | "stopped", "pid"?: number, "reason"?: string }`
- `gateway-status` payload: `{ "state": "starting" | "running" | "restarting" | "failed" | "stopped", "pid"?: number, "reason"?: string }`
- `log` payload: arbitrary log line; optional, can be disabled.
## Suggested client flow
@@ -40,4 +45,4 @@ Each line is a JSON object. Two shapes exist:
4) For user toggles, send `set-heartbeats` and await response.
## Backward compatibility
- If the control port is unavailable (older relay), the client may fall back to the legacy CLI path, but the intended path is to rely solely on this API.
- If the control port is unavailable (older gateway), the client may fall back to the legacy CLI path, but the intended path is to rely solely on this API.

View File

@@ -1,10 +1,15 @@
---
summary: "Runbook for the Gateway daemon, lifecycle, and operations"
read_when:
- Running or debugging the gateway process
---
# Gateway (daemon) runbook
Last updated: 2025-12-09
## What it is
- The always-on process that owns the single Baileys/Telegram connection and the control/event plane.
- Replaces the legacy `relay` command. CLI entry point: `clawdis gateway`.
- Replaces the legacy `gateway` command. CLI entry point: `clawdis gateway`.
- Runs until stopped; exits non-zero on fatal errors so the supervisor restarts it.
## How to run (local)
@@ -128,5 +133,5 @@ Enable with `systemctl enable --now clawdis-gateway.service`.
- `clawdis gw:call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
## Migration guidance
- Retire uses of `clawdis relay` and the legacy TCP control port.
- Retire uses of `clawdis gateway` and the legacy TCP control port.
- Update clients to speak the WS protocol with mandatory hello and structured presence.

View File

@@ -1,3 +1,8 @@
---
summary: "Telegram Bot API integration via grammY with setup notes"
read_when:
- Working on Telegram or grammY pathways
---
# grammY Integration (Telegram Bot API)
Updated: 2025-12-07
@@ -8,8 +13,8 @@ Updated: 2025-12-07
- Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
# What we shipped
- **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + relay) with the grammY throttler enabled by default.
- **Relay:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
- **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default.
- **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
- **Proxy:** optional `telegram.proxy` uses `undici.ProxyAgent` through grammYs `client.baseFetch`.
- **Webhook helpers:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown and optional `--webhook-url` override.
- **Sessions:** direct chats map to `main`; groups map to `group:<chatId>`; replies route back to the same surface.

View File

@@ -1,3 +1,8 @@
---
summary: "Behavior and config for WhatsApp group message handling"
read_when:
- Changing group message rules or mentions
---
# Group messages (web provider)
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
@@ -46,7 +51,7 @@ Notes:
- Manual smoke:
- Send an `@clawd` ping in the group and confirm a reply that references the sender name.
- Send a second ping and verify the history block is included then cleared on the next turn.
- Check relay logs (run with `--verbose`) to see `inbound web message (batched)` entries showing `from: <groupJid>` and the `[from: …]` suffix.
- Check gateway logs (run with `--verbose`) to see `inbound web message (batched)` entries showing `from: <groupJid>` and the `[from: …]` suffix.
## Known considerations
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.

View File

@@ -1,3 +1,8 @@
---
summary: "Health check steps for Baileys/WhatsApp connectivity"
read_when:
- Diagnosing web provider health
---
# Health Checks (CLI)
Short guide to verify the WhatsApp Web / Baileys stack without guessing.
@@ -12,12 +17,12 @@ Short guide to verify the WhatsApp Web / Baileys stack without guessing.
## Deep diagnostics
- Creds on disk: `ls -l ~/.clawdis/credentials/creds.json` (mtime should be recent).
- Session store: `ls -l ~/.clawdis/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
- IPC socket (if relay is running): `ls -l ~/.clawdis/clawdis.sock`.
- IPC socket (if gateway is running): `ls -l ~/.clawdis/clawdis.sock`.
- Relink flow: `pnpm clawdis logout && pnpm clawdis login --provider web --verbose` when status codes 409515 or `loggedOut` appear in logs.
## When something fails
- `logged out` or status 409515 → relink with `clawdis logout` then `clawdis login --provider web`.
- Repeated reconnect exits → tune `web.reconnect` (flags: `--web-retries`, `--web-retry-initial`, `--web-retry-max`) and rerun relay.
- Repeated reconnect exits → tune `web.reconnect` (flags: `--web-retries`, `--web-retry-initial`, `--web-retry-max`) and rerun gateway.
- No inbound messages → confirm linked phone is online and sender is allowed; use `pnpm clawdis heartbeat --all --verbose` to test each known recipient.
## Dedicated "health" command

View File

@@ -1,3 +1,8 @@
---
summary: "Plan for heartbeat polling messages and notification rules"
read_when:
- Adjusting heartbeat cadence or messaging
---
# Heartbeat polling plan (2025-11-26)
Goal: add a simple heartbeat poll for command-based auto-replies (Pi/Tau) that only notifies users when something matters, using the `HEARTBEAT_OK` sentinel. The heartbeat body we send is `HEARTBEAT /think:high` so the model can easily spot it.
@@ -12,10 +17,10 @@ Goal: add a simple heartbeat poll for command-based auto-replies (Pi/Tau) that o
- New optional idle override for heartbeats: `inbound.reply.session.heartbeatIdleMinutes` (defaults to `idleMinutes`). Heartbeat skips do **not** update the session `updatedAt` so idle expiry still works.
## Poller behavior
- When relay runs with command-mode auto-reply, start a timer with the resolved heartbeat interval.
- When gateway runs with command-mode auto-reply, start a timer with the resolved heartbeat interval.
- Each tick invokes the configured command with a short heartbeat body (e.g., “(heartbeat) summarize any important changes since last turn”) while reusing the active session args so Pi context stays warm.
- Heartbeats never create a new session implicitly: if theres no stored session for the target (fallback path), the heartbeat is skipped instead of starting a fresh Pi session.
- Abort timer on SIGINT/abort of the relay.
- Abort timer on SIGINT/abort of the gateway.
## Sentinel handling
- Trim output. If the trimmed text equals `HEARTBEAT_OK` (case-sensitive) -> skip outbound message.
@@ -39,6 +44,6 @@ Goal: add a simple heartbeat poll for command-based auto-replies (Pi/Tau) that o
- Expose CLI triggers:
- `clawdis heartbeat` (web provider, defaults to first `allowFrom`; optional `--to` override)
- `--session-id <uuid>` forces resuming a specific session for that heartbeat
- `clawdis relay --heartbeat-now` to run the relay loop with an immediate heartbeat
- Relay supports `--heartbeat-now` to fire once at startup.
- `clawdis gateway --heartbeat-now` to run the gateway loop with an immediate heartbeat
- Gateway supports `--heartbeat-now` to fire once at startup.
- When multiple sessions are active or `allowFrom` is only `"*"`, require `--to <E.164>` or `--all` for manual heartbeats to avoid ambiguous targets.

View File

@@ -1,6 +1,11 @@
---
summary: "Image and media handling rules for send, gateway, and agent replies"
read_when:
- Modifying media pipeline or attachments
---
# Image & Media Support — 2025-12-05
CLAWDIS is now **web-only** (Baileys). This document captures the current media handling rules for send, relay, and agent replies.
CLAWDIS is now **web-only** (Baileys). This document captures the current media handling rules for send, gateway, and agent replies.
## Goals
- Send media with optional captions via `clawdis send --media`.

View File

@@ -1,3 +1,8 @@
---
summary: "Top-level overview of Clawdis, features, and purpose"
read_when:
- Introducing Clawdis to newcomers
---
# CLAWDIS 🦞
> *"EXFOLIATE! EXFOLIATE!"* — A space lobster, probably
@@ -8,7 +13,7 @@
## What is this?
CLAWDIS (née Warelay) bridges WhatsApp to AI coding agents like [Tau/Pi](https://github.com/badlogic/pi-mono). Send a message, get an AI response. It's like having a genius lobster on call 24/7.
CLAWDIS bridges WhatsApp to AI coding agents like [Tau/Pi](https://github.com/badlogic/pi-mono). Send a message, get an AI response. It's like having a genius lobster on call 24/7.
```
┌─────────────┐ ┌──────────┐ ┌─────────────┐
@@ -66,9 +71,9 @@ clawdis status
- [Security](./security.md) — Keeping your lobster safe
- [Troubleshooting](./troubleshooting.md) — When the CLAWDIS misbehaves
## Why "Warelay"?
## Why "Clawdis"?
The original name was **Warelay** (WhatsApp + Relay). It worked. It was fine.
The original name was **Clawdis** (WhatsApp + Gateway). It worked. It was fine.
But then Clawd happened, and suddenly we needed something with more... *personality*.

View File

@@ -1,10 +1,15 @@
---
summary: "Backstory and lore of Clawdis for context and tone"
read_when:
- Writing docs or UX copy that reference lore
---
# The Lore of CLAWDIS 🦞📖
*A tale of lobsters, time machines, and too many tokens.*
## The Origin Story
In the beginning, there was **Warelay** — a sensible name for a WhatsApp relay. It did its job. It was fine.
In the beginning, there was **Clawdis** — a sensible name for a WhatsApp gateway. It did its job. It was fine.
But then came **Clawd**.

View File

@@ -1,18 +1,23 @@
# Clawdis relay as a child process of the macOS app
---
summary: "Running the gateway as a child process of the macOS app and why"
read_when:
- Integrating the mac app with the gateway lifecycle
---
# Clawdis gateway as a child process of the macOS app
Date: 2025-12-06 · Status: draft · Owner: steipete
## Goal
Run the Node-based Clawdis/clawdis relay as a direct child of the LSUIElement app (instead of a launchd agent) while keeping all TCC-sensitive work inside the Swift app/XPC and wiring the existing “Clawdis Active” toggle to start/stop the child.
Run the Node-based Clawdis/clawdis gateway as a direct child of the LSUIElement app (instead of a launchd agent) while keeping all TCC-sensitive work inside the Swift app/XPC and wiring the existing “Clawdis Active” toggle to start/stop the child.
## When to prefer the child-process mode
- You want relay lifetime strictly coupled to the menu-bar app (dies when the app quits) and controlled by the “Clawdis Active” toggle without touching launchd.
- You want gateway lifetime strictly coupled to the menu-bar app (dies when the app quits) and controlled by the “Clawdis Active” toggle without touching launchd.
- Youre okay giving up login persistence/auto-restart that launchd provides, or youll add your own backoff loop.
- You want simpler log capture and supervision inside the app (no external plist or user-visible LaunchAgent).
## Tradeoffs vs. launchd
- **Pros:** tighter coupling to UI state; simpler surface (no plist install/bootout); easier to stream stdout/stderr; fewer moving parts for beta users.
- **Cons:** no built-in KeepAlive/login auto-start; app crash kills relay; you must build your own restart/backoff; Activity Monitor will show both processes under the app; still need correct TCC handling (see below).
- **Cons:** no built-in KeepAlive/login auto-start; app crash kills gateway; you must build your own restart/backoff; Activity Monitor will show both processes under the app; still need correct TCC handling (see below).
- **TCC:** behaviorally, child processes often inherit the parent apps “responsible process” for TCC, but this is *not a contract*. Continue to route all protected actions through the Swift app/XPC so prompts stay tied to the signed app bundle.
## TCC guardrails (must keep)
@@ -29,10 +34,10 @@ Run the Node-based Clawdis/clawdis relay as a direct child of the LSUIElement ap
- Add a small `RelayProcessManager` (Swift) that owns:
- `execution: Execution?` from `Swift Subprocess` to track the child.
- `start(config)` called when “Clawdis Active” flips ON:
- binary: host Node running the bundled relay under `Clawdis.app/Contents/Resources/Relay/`
- binary: host Node running the bundled gateway under `Clawdis.app/Contents/Resources/Gateway/`
- args: current clawdis entrypoint and flags
- cwd/env: point to `~/.clawdis` as today; inject the expanded PATH so Homebrew Node resolves under launchd
- output: stream stdout/stderr to `/tmp/clawdis-relay.log` (cap buffer via Subprocess OutputLimits)
- output: stream stdout/stderr to `/tmp/clawdis-gateway.log` (cap buffer via Subprocess OutputLimits)
- restart: optional linear/backoff restart if exit was non-zero and Active is still true
- `stop()` called when Active flips OFF or app terminates: cancel the execution and `waitUntilExit`.
- Wire SwiftUI toggle:
@@ -41,19 +46,19 @@ Run the Node-based Clawdis/clawdis relay as a direct child of the LSUIElement ap
- Keep the existing `LaunchdManager` around so we can switch back if needed; the toggle can choose between launchd or child mode with a flag if we want both.
## Packaging and signing
- Bundle the relay payload (dist + production node_modules) under `Contents/Resources/Relay/`; rely on host Node ≥22 instead of embedding a runtime.
- Bundle the gateway payload (dist + production node_modules) under `Contents/Resources/Gateway/`; rely on host Node ≥22 instead of embedding a runtime.
- Codesign native addons and dylibs inside the bundle; no nested runtime binary to sign now.
- Host runtime should not call TCC APIs directly; keep privileged work inside the app/XPC.
## Logging and observability
- Stream child stdout/stderr to `/tmp/clawdis-relay.log`; surface the last N lines in the Debug tab.
- Stream child stdout/stderr to `/tmp/clawdis-gateway.log`; surface the last N lines in the Debug tab.
- Emit a user notification (via existing NotificationManager) on crash/exit while Active is true.
- Add a lightweight heartbeat from Node → app (e.g., ping over stdout) so the app can show status in the menu.
## Failure/edge cases
- App crash/quit kills the relay. Decide if that is acceptable for the deployment tier; otherwise, stick with launchd for production and keep child-process for dev/experiments.
- If the relay exits repeatedly, back off (e.g., 1s/2s/5s/10s) and give up after N attempts with a menu warning.
- Respect the existing pause semantics: when paused, the XPC should return `ok=false, "clawdis paused"`; the relay should avoid calling privileged routes while paused.
- App crash/quit kills the gateway. Decide if that is acceptable for the deployment tier; otherwise, stick with launchd for production and keep child-process for dev/experiments.
- If the gateway exits repeatedly, back off (e.g., 1s/2s/5s/10s) and give up after N attempts with a menu warning.
- Respect the existing pause semantics: when paused, the XPC should return `ok=false, "clawdis paused"`; the gateway should avoid calling privileged routes while paused.
## Open questions / follow-ups
- Do we need dual-mode (launchd for prod, child for dev)? If yes, gate via a setting or build flag.
@@ -62,5 +67,5 @@ Run the Node-based Clawdis/clawdis relay as a direct child of the LSUIElement ap
## Decision snapshot (current recommendation)
- Keep all TCC surfaces in the Swift app/XPC.
- Implement `RelayProcessManager` with Swift Subprocess to start/stop the relay on the “Clawdis Active” toggle.
- Implement `RelayProcessManager` with Swift Subprocess to start/stop the gateway on the “Clawdis Active” toggle.
- Maintain the launchd path as a fallback for uptime/login persistence until child-mode proves stable.

View File

@@ -1,3 +1,8 @@
---
summary: "How the macOS app reports gateway/Baileys health states"
read_when:
- Debugging mac app health indicators
---
# Health Checks on macOS
How to see whether the WhatsApp Web/Baileys bridge is healthy from the menu bar app.
@@ -19,4 +24,4 @@ How to see whether the WhatsApp Web/Baileys bridge is healthy from the menu bar
- Cache the last good snapshot and the last error separately to avoid flicker; show the timestamp of each.
## When in doubt
- You can still use the CLI flow in `docs/health.md` (status, heartbeat dry-run, relay heartbeat) and tail `/tmp/clawdis/clawdis.log` for `web-heartbeat` / `web-reconnect`.
- You can still use the CLI flow in `docs/health.md` (status, heartbeat dry-run, gateway heartbeat) and tail `/tmp/clawdis/clawdis.log` for `web-heartbeat` / `web-reconnect`.

View File

@@ -1,3 +1,8 @@
---
summary: "Menu bar icon states and animations for Clawdis on macOS"
read_when:
- Changing menu bar icon behavior
---
# Menu Bar Icon States
Author: steipete · Updated: 2025-12-06 · Scope: macOS app (`apps/macos`)

View File

@@ -1,3 +1,8 @@
---
summary: "Enabling verbose macOS unified logging for Clawdis with privacy flags"
read_when:
- Capturing macOS logs or investigating private data logging
---
# Logging private data on macOS
Unified logging redacts most payloads unless a subsystem opts into `privacy -off`. Per Peter's write-up on macOS [logging privacy shenanigans](https://steipete.me/posts/2025/logging-privacy-shenanigans) (2025) this is controlled by a plist in `/Library/Preferences/Logging/Subsystems/` keyed by the subsystem name. Only new log entries pick up the flag, so enable it before reproducing an issue.

View File

@@ -1,3 +1,8 @@
---
summary: "Menu bar status logic and what is surfaced to users"
read_when:
- Tweaking mac menu UI or status logic
---
# Menu Bar Status Logic
## What is shown

View File

@@ -1,8 +1,13 @@
---
summary: "macOS app flow for controlling a remote Clawdis gateway over SSH"
read_when:
- Setting up or debugging remote mac control
---
# Remote Clawdis (macOS ⇄ remote host)
Updated: 2025-12-08
This flow lets the macOS app act as a full remote control for a Clawdis relay running on another host (e.g. a Mac Studio). All features—health checks, permissions bootstrapping via the helper CLI, Voice Wake forwarding, and Web Chat—reuse the same remote SSH configuration from *Settings → General*.
This flow lets the macOS app act as a full remote control for a Clawdis gateway running on another host (e.g. a Mac Studio). All features—health checks, permissions bootstrapping via the helper CLI, Voice Wake forwarding, and Web Chat—reuse the same remote SSH configuration from *Settings → General*.
## Modes
- **Local (this Mac)**: Everything runs on the laptop. No SSH involved.
@@ -23,8 +28,8 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru
4) Health checks and Web Chat will now run through this SSH tunnel automatically.
## Web Chat over SSH
- The relay hosts a loopback-only HTTP server (default 18788, see `webchat.port`).
- The mac app forwards `127.0.0.1:<port>` over SSH (`ssh -L <ephemeral>:127.0.0.1:<port>`), then loads `/webchat/?session=<key>` in-app. Sends go in-process on the relay (no CLI spawn/PATH issues).
- The gateway hosts a loopback-only HTTP server (default 18788, see `webchat.port`).
- The mac app forwards `127.0.0.1:<port>` over SSH (`ssh -L <ephemeral>:127.0.0.1:<port>`), then loads `/webchat/?session=<key>` in-app. Sends go in-process on the gateway (no CLI spawn/PATH issues).
- Keep the feature enabled in *Settings → Config → Web chat*. Disable it to hide the menu entry entirely.
## Permissions
@@ -38,14 +43,14 @@ This flow lets the macOS app act as a full remote control for a Clawdis relay ru
## Troubleshooting
- **exit 127 / not found**: `clawdis` isnt on PATH for non-login shells. Add it to `/etc/paths`, your shell rc, or symlink into `/usr/local/bin`/`/opt/homebrew/bin`.
- **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`clawdis status --json`).
- **Web Chat stuck**: confirm the relay is running on the remote host and `webchat.enabled` is true; ensure the forwarded port matches *Settings → Config*. Since RPC is in-process, PATH is no longer a factor.
- **Web Chat stuck**: confirm the gateway is running on the remote host and `webchat.enabled` is true; ensure the forwarded port matches *Settings → Config*. Since RPC is in-process, PATH is no longer a factor.
- **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed.
## Notification sounds
Pick sounds per notification from scripts with the helper CLI, e.g.:
```bash
clawdis-mac notify --title "Ping" --body "Remote relay ready" --sound Glass
clawdis-mac notify --title "Ping" --body "Remote gateway ready" --sound Glass
```
There is no global “default sound” toggle in the app anymore; callers choose a sound (or none) per request.

View File

@@ -1,3 +1,8 @@
---
summary: "Signing steps for macOS debug builds generated by packaging scripts"
read_when:
- Building or signing mac debug builds
---
# mac signing (debug builds)
This app is usually built from `scripts/package-mac-app.sh`, which now:

View File

@@ -1,3 +1,8 @@
---
summary: "Voice overlay lifecycle when wake-word and push-to-talk overlap"
read_when:
- Adjusting voice overlay behavior
---
## Voice Overlay Lifecycle (macOS)
Audience: macOS app contributors. Goal: keep the voice overlay predictable when wake-word and push-to-talk overlap.

View File

@@ -1,3 +1,8 @@
---
summary: "Voice wake and push-to-talk modes plus routing details in the mac app"
read_when:
- Working on voice wake or PTT pathways
---
# Voice Wake & Push-to-Talk
Updated: 2025-12-08 · Owners: mac app

View File

@@ -1,6 +1,11 @@
---
summary: "How the mac app embeds the gateway WebChat and how to debug it"
read_when:
- Debugging mac WebChat view or loopback port
---
# Web Chat (macOS app)
The macOS menu bar app opens the relays loopback web chat server in a WKWebView. It reuses the **primary Clawd session** (`main` by default, configurable via `inbound.reply.session.mainKey`). The server is started by the Node relay (default port 18788, see `webchat.port`).
The macOS menu bar app opens the gateways loopback web chat server in a WKWebView. It reuses the **primary Clawd session** (`main` by default, configurable via `inbound.reply.session.mainKey`). The server is started by the Node gateway (default port 18788, see `webchat.port`).
## Launch & debugging
- Manual: Lobster menu → “Open Chat”.
@@ -9,12 +14,12 @@ The macOS menu bar app opens the relays loopback web chat server in a WKWebVi
- WK logs: navigation lifecycle, readyState, js location, and JS errors/unhandled rejections are mirrored to OSLog for easier diagnosis.
## How its wired
- Assets: `apps/macos/Sources/Clawdis/Resources/WebChat/` contains the `pi-web-ui` dist plus a local import map pointing at bundled vendor modules and a tiny `pi-ai` stub. Everything is served from the relay at `/webchat/*`.
- Bridge: none. The web UI calls `/webchat/rpc` directly; Swift no longer proxies messages. RPC is handled in-process inside the relay (no CLI spawn/PATH dependency).
- Assets: `apps/macos/Sources/Clawdis/Resources/WebChat/` contains the `pi-web-ui` dist plus a local import map pointing at bundled vendor modules and a tiny `pi-ai` stub. Everything is served from the gateway at `/webchat/*`.
- Bridge: none. The web UI calls `/webchat/rpc` directly; Swift no longer proxies messages. RPC is handled in-process inside the gateway (no CLI spawn/PATH dependency).
- Session: always primary; multiple transports (WhatsApp/Telegram/Desktop) share the same session key so context is unified.
## Security / surface area
- Loopback server only; remote mode uses SSH port-forwarding from the relay host to the Mac. CSP is set to `default-src 'self' 'unsafe-inline' data: blob:`.
- Loopback server only; remote mode uses SSH port-forwarding from the gateway host to the Mac. CSP is set to `default-src 'self' 'unsafe-inline' data: blob:`.
- Web Inspector is opt-in via right-click; otherwise WKWebView stays in the app sandbox.
## Known limitations

View File

@@ -1,8 +1,13 @@
---
summary: "macOS XPC architecture for Clawdis app, CLI helper, and gateway bridge"
read_when:
- Editing XPC contracts or menu bar app IPC
---
# Clawdis macOS XPC architecture (Dec 2025)
## Goals
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
- A small surface for automation: the `clawdis-mac` CLI and the Node relay talk to the app via a local XPC channel.
- A small surface for automation: the `clawdis-mac` CLI and the Node gateway talk to the app via a local XPC channel.
- Predictable permissions: always the same signed bundle ID, launched by launchd, so TCC grants stick.
- Limit who can connect: only signed clients from our team (with a same-UID fallback for development).
@@ -10,7 +15,7 @@
- The app registers a Mach service named `com.steipete.clawdis.xpc` via a user LaunchAgent at `~/Library/LaunchAgents/com.steipete.clawdis.plist`.
- The launch agent runs `dist/Clawdis.app/Contents/MacOS/Clawdis` with `RunAtLoad=true`, `KeepAlive=false`, and a `MachServices` entry for the XPC name.
- The app hosts the XPC listener (`NSXPCListener(machServiceName:)`) and exports `ClawdisXPCService`.
- The CLI (`clawdis-mac`) connects with `NSXPCConnection(machServiceName:)`; the Node relay shells out to the CLI.
- The CLI (`clawdis-mac`) connects with `NSXPCConnection(machServiceName:)`; the Node gateway shells out to the CLI.
- Security: on incoming connections we read the audit token (or PID) and allow only:
- Code-signed clients with team ID `Y5PE65HELJ`; or
- Same-UID processes (fallback to avoid blocking local dev).

View File

@@ -1,3 +1,8 @@
---
summary: "Command queue design that serializes auto-reply command execution"
read_when:
- Changing auto-reply execution or concurrency
---
# Command Queue (2025-11-25)
We now serialize all command-based auto-replies (WhatsApp Web listener) through a tiny in-process queue to prevent multiple commands from running at once.

View File

@@ -1,8 +1,13 @@
---
summary: "Implementation plan for the new gateway architecture and protocol"
read_when:
- Executing the gateway refactor
---
# New Gateway Architecture Implementation Plan (detailed)
Last updated: 2025-12-09
Goal: replace legacy relay/stdin/TCP control with a single WebSocket Gateway, typed protocol, and first-frame snapshot. No backward compatibility.
Goal: replace legacy gateway/stdin/TCP control with a single WebSocket Gateway, typed protocol, and first-frame snapshot. No backward compatibility.
---

View File

@@ -1,14 +1,19 @@
# Web Relay Troubleshooting (Nov 26, 2025)
---
summary: "Troubleshooting guide for the web gateway/Baileys relay"
read_when:
- Diagnosing web relay socket or login issues
---
# Web Gateway Troubleshooting (Nov 26, 2025)
## Symptoms & quick fixes
- **Stream Errored / Conflict / status 409515:** WhatsApp closed the socket because another session is active or creds went stale. Run `clawdis logout` then `clawdis login --provider web` and restart the relay.
- **Stream Errored / Conflict / status 409515:** WhatsApp closed the socket because another session is active or creds went stale. Run `clawdis logout` then `clawdis login --provider web` and restart the gateway.
- **Logged out:** Console prints “session logged out”; re-link with `clawdis login --provider web`.
- **Repeated retries then exit:** Reconnects are capped (default 12 attempts). Tune with `--web-retries`, `--web-retry-initial`, `--web-retry-max`, or config `web.reconnect`.
- **No inbound messages:** Ensure the QR-linked account is online in WhatsApp, and check logs for `web-heartbeat` to confirm auth age/connection.
- **Fast nuke:** From an allowed WhatsApp sender you can send `/restart` to kick `com.steipete.clawdis` via launchd; wait a few seconds for it to relink.
## Helpful commands
- Start relay web-only: `pnpm clawdis gateway --provider web --verbose`
- Start gateway web-only: `pnpm clawdis gateway --provider web --verbose`
- Show who is linked: `pnpm clawdis gateway --provider web --verbose` (first line prints the linked E.164)
- Logout (clear creds): `pnpm clawdis logout`
- Relink: `pnpm clawdis login --provider web`

View File

@@ -1,9 +1,14 @@
---
summary: "Remote mode topology using SSH control channels between gateway and mac app"
read_when:
- Running or troubleshooting remote gateway setups
---
# Remote mode with control channel
This repo supports “remote over SSH” by keeping a single relay (the master) running on a host (e.g., your Mac Studio) and connecting one or more macOS menu bar clients to it. The menu app no longer shells out to `pnpm clawdis …`; it talks to the relay over a persistent control channel that is tunneled through SSH.
This repo supports “remote over SSH” by keeping a single gateway (the master) running on a host (e.g., your Mac Studio) and connecting one or more macOS menu bar clients to it. The menu app no longer shells out to `pnpm clawdis …`; it talks to the gateway over a persistent control channel that is tunneled through SSH.
## Topology
- Master: runs the relay + control server on `127.0.0.1:18789` (in-process TCP server).
- Master: runs the gateway + control server on `127.0.0.1:18789` (in-process TCP server).
- Clients: when “Remote over SSH” is selected, the app opens one SSH tunnel:
- `ssh -N -L <localPort>:127.0.0.1:18789 <user>@<host>`
- The app then connects to `localhost:<localPort>` and keeps that socket open.
@@ -14,10 +19,10 @@ This repo supports “remote over SSH” by keeping a single relay (the master)
2) Open TCP socket to the local forwarded port.
3) Send `ping` to verify connectivity.
4) Issue `health`, `status`, and `last-heartbeat` requests to seed UI.
5) Listen for `event` frames (heartbeat updates, relay status).
5) Listen for `event` frames (heartbeat updates, gateway status).
## Heartbeats
- Heartbeats always run on the master relay.
- Heartbeats always run on the master gateway.
- The control server emits `event: "heartbeat"` after each heartbeat attempt and keeps the latest in memory for `last-heartbeat` requests.
- No file-based heartbeat logs/state are required when the control stream is available.
@@ -26,7 +31,7 @@ This repo supports “remote over SSH” by keeping a single relay (the master)
## Failure handling
- If the tunnel drops, the client reconnects and re-issues `ping`, `health`, and `last-heartbeat` to refresh state (the mac app shows “Control channel disconnected”).
- If the control port is unavailable (older relay), the app can optionally fall back to the legacy CLI path, but the goal is to rely solely on the control channel.
- If the control port is unavailable (older gateway), the app can optionally fall back to the legacy CLI path, but the goal is to rely solely on the control channel.
## Test Remote (in the mac app)
1) SSH reachability check (`ssh -o BatchMode=yes … echo ok`).
@@ -39,4 +44,4 @@ This repo supports “remote over SSH” by keeping a single relay (the master)
## Files to keep in sync
- Protocol definition: `docs/control-api.md`.
- App connection logic: macOS `Remote over SSH` plumbing.
- Relay control server: lives inside the Node relay process.
- Gateway control server: lives inside the Node gateway process.

View File

@@ -1,6 +1,11 @@
---
summary: "JSON RPC contract used by the mac app to talk to the gateway"
read_when:
- Changing mac app RPC or agent toggles
---
# Clawdis Agent RPC
Live, stdin/stdout JSON RPC used by the mac app (XPC) to avoid spawning `clawdis agent --json` for every send and to toggle runtime features (e.g., heartbeats) without restarting the relay.
Live, stdin/stdout JSON RPC used by the mac app (XPC) to avoid spawning `clawdis agent --json` for every send and to toggle runtime features (e.g., heartbeats) without restarting the gateway.
## How it is launched
- The mac app starts `clawdis rpc` in the configured project root (`CommandResolver.projectRoot()`, defaults to `~/Projects/clawdis`).
@@ -11,7 +16,7 @@ Live, stdin/stdout JSON RPC used by the mac app (XPC) to avoid spawning `clawdis
### Requests (stdin)
- `{"type":"status"}` → health ping.
- `{"type":"send","text":"hi","session":"main","thinking":"low","deliver":false,"to":"+1555..."}` → invokes existing agent send path.
- `{"type":"set-heartbeats","enabled":true|false}` → enables/disables web heartbeat timers in the running relay process.
- `{"type":"set-heartbeats","enabled":true|false}` → enables/disables web heartbeat timers in the running gateway process.
### Responses (stdout)
- `{"type":"result","ok":true,"payload":{...}}` on success.
@@ -23,8 +28,8 @@ Notes:
## Heartbeat control (new)
- The mac menu exposes “Send heartbeats” toggle (persisted in UserDefaults).
- On change, mac sends `set-heartbeats` RPC; the relay updates an in-memory flag and short-circuits its heartbeat timers (`web-heartbeat` logging + reply heartbeats).
- No relay restart required.
- On change, mac sends `set-heartbeats` RPC; the gateway updates an in-memory flag and short-circuits its heartbeat timers (`web-heartbeat` logging + reply heartbeats).
- No gateway restart required.
## Fallbacks / safety
- If the RPC process is not running, mac-side RPC calls fail fast and the app logs/clears state; callers may fall back to one-shot CLI where appropriate.
@@ -32,5 +37,5 @@ Notes:
## Future extensions
- Add `abort` to cancel in-flight sends.
- Add `compact` / `status --verbose` to return relay internals (queue depth, session info).
- Add `compact` / `status --verbose` to return gateway internals (queue depth, session info).
- Add a JSON schema test for the RPC contract.

View File

@@ -1,3 +1,8 @@
---
summary: "Security considerations and threat model for running an AI gateway with shell access"
read_when:
- Adding features that widen access or automation
---
# Security 🔒
Running an AI agent with shell access on your machine is... *spicy*. Here's how to not get pwned.

View File

@@ -1,3 +1,8 @@
---
summary: "Session management rules, keys, and persistence for chats"
read_when:
- Modifying session handling or storage
---
# Session Management
Clawdis treats **one session as primary**. By default the canonical key is `main` for every direct chat; no configuration is required. You can rename it via `inbound.reply.session.mainKey` if you really want, but there is still only a single primary session. Older/local sessions can stay on disk, but only the primary key is used for desktop/web chat and direct agent calls.

View File

@@ -1,3 +1,8 @@
---
summary: "Routing rules per surface (WhatsApp, Telegram, web) and shared context"
read_when:
- Changing surface routing or inbox behavior
---
# Surfaces & Routing
Updated: 2025-12-07
@@ -9,6 +14,6 @@ Goal: make replies deterministic per channel while keeping one shared context fo
- **Session store:** Keys are resolved via `resolveSessionKey(scope, ctx, mainKey)`; the Tau JSONL path still lives under `~/.clawdis/sessions/<SessionId>.jsonl`.
- **WebChat:** Always attaches to `main`, loads the full Tau transcript so desktop reflects cross-surface history, and writes new turns back to the same session.
- **Implementation hints:**
- Set `Surface` in each ingress (WhatsApp relay, WebChat bridge, future Telegram).
- Set `Surface` in each ingress (WhatsApp gateway, WebChat bridge, future Telegram).
- Keep routing deterministic: originate → same surface. Use IPC/web senders accordingly.
- Do not let the agent emit “send to X” decisions; keep that policy in the host code.

View File

@@ -1,3 +1,8 @@
---
summary: "Telegram bot support status, capabilities, and configuration"
read_when:
- Working on Telegram features or webhooks
---
# Telegram (Bot API)
Updated: 2025-12-07
@@ -12,7 +17,7 @@ Status: ready for bot-mode use with grammY (long-poll + webhook). Text + media s
## How it will work (Bot API)
1) Create a bot with @BotFather and grab the token.
2) Configure Clawdis with `TELEGRAM_BOT_TOKEN` (or `telegram.botToken` in `~/.clawdis/clawdis.json`).
3) Run the relay; it auto-starts Telegram when the bot token is set. To force Telegram-only: `clawdis relay --provider telegram`. Webhook mode: `clawdis relay --provider telegram --webhook --port 8787 --webhook-secret <secret>` (optionally `--webhook-url` when the public URL differs).
3) Run the gateway; it auto-starts Telegram when the bot token is set. To force Telegram-only: `clawdis gateway --provider telegram`. Webhook mode: `clawdis gateway --provider telegram --webhook --port 8787 --webhook-secret <secret>` (optionally `--webhook-url` when the public URL differs).
4) Direct chats: user sends the first message; all subsequent turns land in the shared `main` session (default, no extra config).
5) Groups: add the bot, disable privacy mode (or make it admin) so it can read messages; group threads stay on `group:<chatId>` and require mention/command to trigger replies.
6) Optional allowlist: reuse `inbound.allowFrom` for direct chats by chat id (`123456789` or `telegram:123456789`).
@@ -24,7 +29,7 @@ Status: ready for bot-mode use with grammY (long-poll + webhook). Text + media s
- Typing indicators (`sendChatAction`) supported; inline reply/threading supported where Telegram allows.
## Planned implementation details
- Library: grammY is the only client for send + relay (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
- Library: grammY is the only client for send + gateway (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, and `Timestamp`; groups require @bot mention by default.
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl` supported.
@@ -52,7 +57,7 @@ Example config:
## Roadmap
- ✅ Design and defaults (this doc)
- ✅ grammY long-poll relay + text/media send
- ✅ grammY long-poll gateway + text/media send
- ✅ Proxy + webhook helpers (setWebhook/deleteWebhook, health endpoint, optional public URL)
- ⏳ Add more grammY coverage (webhook payloads, media edge cases)

View File

@@ -1,3 +1,8 @@
---
summary: "Directive syntax for /think levels and how they affect model reasoning"
read_when:
- Adjusting thinking level parsing or defaults
---
# Thinking Levels (/think directives)
## What it does

View File

@@ -1,3 +1,8 @@
---
summary: "Quick troubleshooting guide for common Clawdis failures"
read_when:
- Investigating runtime issues or failures
---
# Troubleshooting 🔧
When your CLAWDIS misbehaves, here's how to fix it.

View File

@@ -1,3 +1,8 @@
---
summary: "TypeBox schemas as the single source of truth for the gateway protocol"
read_when:
- Updating protocol schemas or codegen
---
# TypeBox as Protocol Source of Truth
Last updated: 2025-12-09

View File

@@ -1,3 +1,8 @@
---
summary: "Loopback WebChat server and SSH tunnel usage for chat UI"
read_when:
- Debugging or configuring WebChat access
---
# WebChat (loopback + SSH tunnel)
Updated: 2025-12-09

View File

@@ -1,7 +1,7 @@
{
"name": "clawdis",
"version": "2.0.0",
"description": "WhatsApp relay CLI (Baileys web) with Pi RPC agent",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module",
"main": "dist/index.js",
"bin": {
@@ -9,6 +9,7 @@
},
"scripts": {
"dev": "tsx src/index.ts",
"docs:list": "tsx scripts/docs-list.ts",
"build": "tsc -p tsconfig.json",
"start": "tsx src/index.ts",
"clawdis": "tsx src/index.ts",

146
scripts/docs-list.ts Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env tsx
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const docsListFile = fileURLToPath(import.meta.url);
const docsListDir = dirname(docsListFile);
const DOCS_DIR = join(docsListDir, '..', 'docs');
const EXCLUDED_DIRS = new Set(['archive', 'research']);
function compactStrings(values: unknown[]): string[] {
const result: string[] = [];
for (const value of values) {
if (value === null || value === undefined) {
continue;
}
const normalized = String(value).trim();
if (normalized.length > 0) {
result.push(normalized);
}
}
return result;
}
function walkMarkdownFiles(dir: string, base: string = dir): string[] {
const entries = readdirSync(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (EXCLUDED_DIRS.has(entry.name)) {
continue;
}
files.push(...walkMarkdownFiles(fullPath, base));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(relative(base, fullPath));
}
}
return files.sort((a, b) => a.localeCompare(b));
}
function extractMetadata(fullPath: string): {
summary: string | null;
readWhen: string[];
error?: string;
} {
const content = readFileSync(fullPath, 'utf8');
if (!content.startsWith('---')) {
return { summary: null, readWhen: [], error: 'missing front matter' };
}
const endIndex = content.indexOf('\n---', 3);
if (endIndex === -1) {
return { summary: null, readWhen: [], error: 'unterminated front matter' };
}
const frontMatter = content.slice(3, endIndex).trim();
const lines = frontMatter.split('\n');
let summaryLine: string | null = null;
const readWhen: string[] = [];
let collectingField: 'read_when' | null = null;
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith('summary:')) {
summaryLine = line;
collectingField = null;
continue;
}
if (line.startsWith('read_when:')) {
collectingField = 'read_when';
const inline = line.slice('read_when:'.length).trim();
if (inline.startsWith('[') && inline.endsWith(']')) {
try {
const parsed = JSON.parse(inline.replace(/'/g, '"')) as unknown;
if (Array.isArray(parsed)) {
readWhen.push(...compactStrings(parsed));
}
} catch {
// ignore malformed inline arrays
}
}
continue;
}
if (collectingField === 'read_when') {
if (line.startsWith('- ')) {
const hint = line.slice(2).trim();
if (hint) {
readWhen.push(hint);
}
} else if (line === '') {
// allow blank lines inside the list
} else {
collectingField = null;
}
}
}
if (!summaryLine) {
return { summary: null, readWhen, error: 'summary key missing' };
}
const summaryValue = summaryLine.slice('summary:'.length).trim();
const normalized = summaryValue
.replace(/^['"]|['"]$/g, '')
.replace(/\s+/g, ' ')
.trim();
if (!normalized) {
return { summary: null, readWhen, error: 'summary is empty' };
}
return { summary: normalized, readWhen };
}
console.log('Listing all markdown files in docs folder:');
const markdownFiles = walkMarkdownFiles(DOCS_DIR);
for (const relativePath of markdownFiles) {
const fullPath = join(DOCS_DIR, relativePath);
const { summary, readWhen, error } = extractMetadata(fullPath);
if (summary) {
console.log(`${relativePath} - ${summary}`);
if (readWhen.length > 0) {
console.log(` Read when: ${readWhen.join('; ')}`);
}
} else {
const reason = error ? ` - [${error}]` : '';
console.log(`${relativePath}${reason}`);
}
}
console.log(
'\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above (React hooks, cache directives, database work, tests, etc.), read that doc before coding, and suggest new coverage when it is missing.'
);