@jeffober/thatch 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/user/cli.md CHANGED
@@ -46,7 +46,7 @@ Read-only access to the chat directory your opencode sessions share
46
46
 
47
47
  ```bash
48
48
  thatch chat list # who is registered: name, age, project, topic
49
- thatch chat tail # follow sent and read events, live
49
+ thatch chat tail # follow sent and read events, live (JSONL)
50
50
  thatch chat tail --once # print the last 20 messages and exit
51
51
  thatch chat tail --once --limit 50 # a longer backlog (or --limit all)
52
52
  thatch chat tail --match "rebase" --match "payments" # body must match both
@@ -54,47 +54,41 @@ thatch chat tail --from al --to bob # substring match on names
54
54
  thatch chat tail --since 2026-09-12 --until "2026-09-13 09:00" # a window
55
55
  ```
56
56
 
57
- `chat list` shows every registered session as aligned columns under a
58
- header row: how long since its last heartbeat (a large age means the
59
- session's process is probably gone), its status (`fresh` or `STALE`),
60
- its project, and the topic it registered with - the fastest way to
61
- answer "which session should I talk to about X?" Color is added only
62
- when the output is a terminal; a pipe prints plain aligned text.
63
-
64
- `chat tail` prints the conversation as styled chat cards (labels on a
65
- colored background, names in color, timestamps dimmed; a drawn rule
66
- separates cards, with blank lines on either side). After each name, the
67
- session's registered topic appears in muted italics - so the tail tells
68
- you not just who is talking but what they are working on. In a
69
- terminal, message bodies render as markdown: if `glow` (or `gum`) is
70
- installed, bodies with headings, lists, code blocks, and emphasis are
71
- styled; plain-text bodies print exactly as sent. Piped output - and a
72
- system without either tool - always gets the raw body.
57
+ `chat list` shows every registered session in two sections, Active and
58
+ Stale, each as aligned columns under a header row: how long since its
59
+ last heartbeat, its status (`fresh` or `stale` for opencode sessions;
60
+ `active` or `idle` for Claude Code and Cursor sessions, which only beat
61
+ when a prompt runs), its project, and its topic (the session's title) -
62
+ the fastest way to answer "which session should I talk to about X?" A
63
+ stale session has missed two heartbeats, about a minute: the opencode
64
+ process hosting it has probably stopped. Color is added only when the
65
+ output is a terminal; a pipe prints plain aligned text.
66
+
67
+ `chat tail` prints the conversation as a JSONL event log: one JSON
68
+ object per line, one event per line. Sending a message and reading it
69
+ are separate events, linked by the message `id`. Every field is plain
70
+ data: names as registered, each participant's topic, the message body
71
+ exactly as sent, and ISO-8601 UTC timestamps. No color, no markdown
72
+ rendering, no local time; pipe through `jq` for either.
73
73
 
74
74
  ```text
75
- From Al Go Rithm <cross-session messaging between opencode sessions>
76
- To Brute the Dream Farrier <diagnosing watcher underuse>
77
- When 2026-09-12 14:02 America/Denver
78
-
79
- CI is green on main
80
- ____________________________________________________________
81
-
82
- To broadcast
83
- When 2026-09-12 21:07 America/Denver
84
-
85
- rebasing payments, hold off
86
- ____________________________________________________________
75
+ {"event":"sent","at":"2026-09-12T20:02:11Z","id":41,"from":"al-go-rithm-00001","from_topic":"cross-session messaging","to":"brute-the-dream-farrier-00001","to_topic":"diagnosing watcher underuse","broadcast":false,"body":"CI is green on main"}
76
+ {"event":"read","at":"2026-09-12T20:02:40Z","id":41,"reader":"brute-the-dream-farrier-00001","reader_topic":"diagnosing watcher underuse","from":"al-go-rithm-00001","from_topic":"cross-session messaging"}
77
+ {"event":"sent","at":"2026-09-13T03:07:02Z","id":42,"from":"al-go-rithm-00001","from_topic":"cross-session messaging","to":"kurn-the-typechecker-00001","to_topic":"release QA","broadcast":true,"body":"rebasing payments, hold off"}
78
+ ```
87
79
 
88
- From Kurn the Typechecker <release QA>
89
- read Marlowe the Cherry Picker <payments refactor>
90
- When 2026-09-12 21:08 America/Denver
80
+ A broadcast is one `sent` event per recipient, each with `broadcast:
81
+ true` and its real `to`, so every copy tracks its own read. Handy
82
+ one-liners:
91
83
 
92
- direct ping
84
+ ```bash
85
+ thatch chat tail | jq -r '"\(.at) \(.event) \(.from) -> \(.to // .reader): \(.body // "")"'
86
+ thatch chat tail --once --limit all | jq -c 'select(.event == "read")'
93
87
  ```
94
88
 
95
- Follow mode runs until Ctrl-C. Read events appear only in follow mode
96
- (they fire when an inbox is drained after the tail started). When
97
- timestamps are in your local timezone. The chat itself flows through the
89
+ Follow mode runs until Ctrl-C. The backlog includes the `read` event of
90
+ every message already read; in follow mode, reads appear as inboxes
91
+ drain. The chat itself flows through the
98
92
  agents (the CLI never sends, reads, or registers on a session's behalf;
99
93
  that happens from opencode sessions via the `thatch_chat_*` tools).
100
94
 
@@ -108,10 +102,8 @@ too, so a filtered tail keeps watching the same way:
108
102
  together: every one must match the message body.
109
103
  - `--from NAME` and `--to NAME` match participant names (case doesn't
110
104
  matter). A departed sender can only be found as the `unknown`
111
- rendering the tail shows. Broadcast cards say `broadcast` in the To
112
- slot, but the row underneath keeps the real recipient: `--to <name>`
113
- finds the fan-out rows that reached them, while `--to broadcast`
114
- matches nothing.
105
+ rendering the tail shows. Broadcast events carry their real recipient,
106
+ so `--to <name>` finds the fan-out copies that reached them.
115
107
  - `--since DT` and `--until DT` bound the window; a message counts if
116
108
  it was sent at or after `--since` and before `--until`. The date
117
109
  format is `YYYY-MM-DD` with an optional `HH:MM` in your local
@@ -119,9 +111,9 @@ too, so a filtered tail keeps watching the same way:
119
111
 
120
112
  A `--until` already in the past exits after the backlog (a closed
121
113
  window has nothing left to follow); one in the future follows until
122
- the window closes. Note that the limit hides sent cards, not their
123
- reads: a hidden message that gets read during follow mode still emits
124
- its read card.
114
+ the window closes. In the backlog, a hidden message's read is hidden
115
+ with it; in follow mode, a hidden message that gets read still emits
116
+ its read event.
125
117
 
126
118
  ## Priming a new project
127
119
 
@@ -1,6 +1,7 @@
1
1
  # Cross-Session Chat
2
2
 
3
- Cross-session chat lets your opencode sessions talk to each other. If one
3
+ Cross-session chat lets your agent sessions talk to each other, in
4
+ opencode, Claude Code, and Cursor. If one
4
5
  session is QAing a release and another is planning a feature, the second can
5
6
  ask the first "is the release green yet?" instead of you relaying messages
6
7
  between tabs all afternoon.
@@ -8,9 +9,9 @@ between tabs all afternoon.
8
9
  ## What it does
9
10
 
10
11
  opencode sessions join the directory automatically, on their first idle
11
- moment. Names are assigned by thatch, never chosen: a lowercase slug of the
12
- session's title plus a counter (a session titled "Fix auth bug" becomes
13
- `fix-auth-bug-00001`). The counter only ever increments, so a name is
12
+ moment. Names are assigned by thatch, never chosen: a name drawn from a
13
+ built-in pool plus a counter (`al-go-rithm-00001`). The counter only ever
14
+ increments, so a name is
14
15
  minted exactly once per machine - pruning an old session never reissues its
15
16
  name to someone else, and the same name always refers to the same session.
16
17
  The live title rides along as the session's topic (refreshed as the
@@ -19,12 +20,14 @@ board without anyone filling in forms. A session can also join or rejoin
19
20
  explicitly with `thatch_chat_register` (no arguments) - useful after
20
21
  `chat_unregister`, or just to check your assigned name. Once registered:
21
22
 
22
- - `thatch_chat_list` shows every registered session on the machine, with a
23
- liveness marker (fresh or stale), how long ago it last checked in, its
24
- project, whether it runs in the project root or a linked git worktree,
25
- its topic, and your unread count
23
+ - `thatch_chat_list` shows every registered session on the machine, grouped
24
+ into Active and Stale sections: active sessions can be woken; stale ones
25
+ have missed two heartbeats, about a minute (the opencode process hosting
26
+ them has probably stopped). Each row carries how long ago the session last
27
+ checked in, its project, whether it runs in the project root or a linked
28
+ git worktree, its topic, and your unread count
26
29
  - `thatch_chat_send` delivers a message to another registered session, by
27
- name or session id
30
+ name or session id, and states the recipient's liveness at send time
28
31
  - `thatch_chat_read` drains the session's inbox
29
32
  - `thatch_chat_broadcast` delivers one message to every other live
30
33
  registered session at once
@@ -56,8 +59,12 @@ toggle reveals those output blocks if you want them.)
56
59
 
57
60
  ## How to use it
58
61
 
59
- With auto-registration you usually do nothing: both sessions are already
60
- in the directory. To introduce them, just tell each session who to talk to:
62
+ With auto-registration you usually do nothing: a session joins the
63
+ directory when it first goes idle, and a session continued with
64
+ `opencode -s <id>` is registered again the moment its harness starts -
65
+ same session id, same name, reclaimed. The agent gets a quiet toast with
66
+ its assigned name when it first registers. To introduce two sessions,
67
+ just tell each one who to talk to:
61
68
 
62
69
  > Ask the other session whether the release has shipped; answer its
63
70
  > questions directly.
@@ -72,14 +79,65 @@ doing. When in doubt it summarizes the mail for you and waits. If you want
72
79
  two sessions to actively cooperate on something, say so explicitly in both
73
80
  sessions.
74
81
 
82
+ ## Claude Code and Cursor
83
+
84
+ Chat works from Claude Code and Cursor through the same MCP server and the
85
+ same shared directory. The seven tools are identical on every host; two
86
+ things differ.
87
+
88
+ **Identity comes from the hook.** Claude Code and Cursor pass no session
89
+ context to MCP tool calls, so the thatch hook anchors identity instead. It
90
+ registers the conversation under a stable id derived from the host's
91
+ session id, and prints `you are NAME (unread count)` at session start and
92
+ on every prompt. The model passes that name as the `as` argument on the
93
+ chat tools. Without the hook (MCP registered by hand, hooks removed), the
94
+ model can still join: `chat_register` with no arguments mints a fresh
95
+ identity for the conversation, and the model uses the returned name from
96
+ there. That identity does not survive a compacted context - re-register
97
+ then, or prefer the hooked path.
98
+
99
+ **Delivery model.** Only opencode can start a turn when mail arrives
100
+ while you are away. The other hosts surface mail at their hook points,
101
+ and both wake the agent when a turn ends:
102
+
103
+ - **Cursor** checks the mailbox when a turn ends (the `stop` hook): unread
104
+ mail is auto-submitted as a follow-up message, so the agent reads it
105
+ without you typing anything. The notification is a pointer (sender
106
+ names + count, never message bodies); bodies flow only through the
107
+ framed `chat_read`. A loop cap bounds consecutive follow-ups, and an
108
+ aborted or errored turn is never auto-continued.
109
+ - **Claude Code** also checks at turn end (the `Stop` hook): unread mail
110
+ arrives as a Stop-hook reminder and the turn continues so the agent
111
+ reads it. The host's own loop protections plus the delivered stamp
112
+ bound repeats, and a session paused on background work is not
113
+ interrupted - the wake lands when that work's turn ends. Mail is also
114
+ announced at startup and resume (the `SessionStart` hook line) and at
115
+ every prompt; if an answer is urgent, say so to the agent directly.
116
+
117
+ Two limits worth knowing:
118
+
119
+ - Cursor cloud agents run no client-side hooks: no identity anchor, no
120
+ mail lines. The MCP tools still work, with a self-registered identity.
121
+ - A conversation idle past the prune window (a week) re-registers under a
122
+ fresh name; mail addressed to the old name is swept.
123
+
124
+ The terminal view ([cli.md](cli.md)) works the same regardless of which
125
+ host the sessions run in.
126
+
75
127
  ## Liveness
76
128
 
77
- A session shows as fresh while its opencode process is running. A crashed
78
- or closed process stops heartbeat-ing, and the session shows as stale in
79
- `thatch_chat_list` after ten minutes. Messages to a stale session wait in
80
- its inbox unread - a dead session never reads them. Explicitly closing a
81
- session (deleting it in the TUI) unregisters it immediately, and a
82
- greenlit `/thatch/exit` unregisters the session too: the wrap-up
129
+ A session shows as fresh while the opencode process hosting it is
130
+ running: hosted sessions keep beating (idle ones included), so the
131
+ freshness is real. When that process dies, its sessions stop beating and
132
+ show as stale about a minute later (two missed heartbeats), grouped into
133
+ a separate Stale section. A laptop that slept reads the same way until
134
+ its processes catch up.
135
+ Messages to a stale session wait in its inbox unread - a dead session
136
+ never reads them, and `chat_send` says so at send time. Continuing a
137
+ session with `opencode -s <id>` reclaims its name and row, and any mail
138
+ that queued while it was down is delivered at startup. Explicitly
139
+ closing a session (deleting it in the TUI) unregisters it immediately,
140
+ and a greenlit `/thatch/exit` unregisters the session too: the wrap-up
83
141
  checklist calls `chat_unregister`, and the plugin removes the row itself
84
142
  when the exit token passes the greenlight check, so other sessions stop
85
143
  addressing mail to a process that is about to vanish.
@@ -132,16 +190,26 @@ construction. Three mitigations:
132
190
  - **Names are assigned, not claimed.** Display names are minted by thatch
133
191
  with a never-reused counter, so a name cannot be grabbed by an unrelated
134
192
  session and an old name cannot be resurrected by an impersonator.
193
+ - **Claude Code identity is anchored by the host process.** Hooks learn
194
+ the true session id from the host and record which process spawned them;
195
+ the MCP server resolves its own parent process against that record, so
196
+ the model cannot claim another session's identity there. On Cursor, chat
197
+ tools still trust the `as` argument the caller supplies (hooks and MCP
198
+ servers share one workspace process, so process identity is ambiguous) -
199
+ a local session that knows a name can act as it. opencode is immune (the
200
+ host supplies identity). The threat model is one user's machine.
135
201
  - **No external content via wake.** The wake notification names senders
136
202
  and counts only - bodies flow exclusively through the framed
137
203
  `chat_read` surface.
138
204
 
139
205
  ## Requirements and limitations
140
206
 
141
- - **Wake-up delivery is opencode only.** The chat tools work everywhere
142
- (other hosts declare their identity with `as`), but only opencode can
143
- start a turn when a message arrives; Claude Code and Cursor see pending
144
- mail at their next prompt (the flush-tools hook line, or `chat_status`).
207
+ - **Idle wake (starting a turn with nobody at the keyboard) is opencode
208
+ only.** The chat tools work everywhere (other hosts declare their
209
+ identity with `as`). Cursor and Claude Code additionally wake the agent
210
+ when a turn ends with mail unread (the `stop`/`Stop` hooks); Claude
211
+ Code also announces mail at startup and resume, and both see pending
212
+ mail at prompt time (the flush-tools hook line, or `chat_status`).
145
213
  - **Same machine only.** The directory and inbox live in thatch's local
146
214
  database. There is no network relay.
147
215
  - **Opt-in.** Unregistered sessions cannot be messaged and cannot send.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/chat-names.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  * unnamed-cast extras). fnord generates its names with an LLM at runtime;
10
10
  * thatch bakes the pool in - static contents, no model call.
11
11
  *
12
- * The pool is the fallback base for registrations with no usable session
13
- * title (ChatStore.register slugifies a random pool name). Pool names are
12
+ * The pool is the base for every registration (ChatStore.register
13
+ * slugifies a random pool name and appends a counter). Pool names are
14
14
  * case-insensitively distinct from each other; slugify is lossy, but a
15
15
  * shared base is harmless - the per-base counter still yields unique
16
16
  * names.