@jeffober/thatch 0.1.44 → 0.1.46
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/README.md +87 -14
- package/bin/thatch +44 -9
- package/docs/dev/README.md +4 -2
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/cicd.md +14 -5
- package/docs/dev/features/commands.md +74 -0
- package/docs/dev/features/cross-session-chat.md +24 -10
- package/docs/dev/features/extraction.md +5 -0
- package/docs/dev/gotchas.md +8 -0
- package/docs/dev/mcp-parity.md +1 -0
- package/docs/dev/setup-and-hooks.md +3 -1
- package/docs/plans/thatch-actions.md +55 -20
- package/docs/user/README.md +5 -1
- package/docs/user/cli.md +12 -3
- package/docs/user/commands.md +50 -0
- package/docs/user/cross-session-chat.md +11 -7
- package/docs/user/setup.md +9 -3
- package/package.json +1 -1
- package/src/chat.ts +32 -4
- package/src/commands.ts +122 -13
- package/src/db.ts +9 -0
- package/src/embeddings.ts +42 -1
- package/src/index.ts +141 -32
- package/src/mcp.ts +62 -3
- package/src/prompts.ts +55 -1
- package/src/setup.ts +9 -0
- package/src/tool-defs.ts +19 -8
- package/docs/plans/nomenclater.md +0 -89
|
@@ -66,6 +66,12 @@ substitution, matching the existing systemPrompt/mcpInstructions convention.
|
|
|
66
66
|
The wrap-up commands (`compact`, `exit`) stay as-is; their checklist text
|
|
67
67
|
becomes a core reused by both templates (already true via `sharedChecklist`).
|
|
68
68
|
|
|
69
|
+
`/thatch/extract` interacts with opencode's direct extraction (parent idle
|
|
70
|
+
spawns the extractor child; the nudge is only a fallback). The action must
|
|
71
|
+
reuse the same accept/complete semantics and must not fight the `extracting`
|
|
72
|
+
set: if direct extraction is already running for the session, the action
|
|
73
|
+
should report that and stop rather than double-dispatch.
|
|
74
|
+
|
|
69
75
|
## Host parity
|
|
70
76
|
|
|
71
77
|
Commands are markdown or MCP prompts, synced per host. Behavior parity is the
|
|
@@ -74,30 +80,52 @@ goal; format differences are documented, not hidden.
|
|
|
74
80
|
- **opencode** (existing): `installOpencodeCommands` syncs markdown to
|
|
75
81
|
`~/.config/opencode/command/thatch/` on every plugin load.
|
|
76
82
|
- **Claude Code**: native user-level slash commands read markdown from
|
|
77
|
-
`~/.claude/commands/thatch/`
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
`~/.claude/commands/thatch/` (user scope) or `.claude/commands/thatch/`
|
|
84
|
+
(project scope), with the same `$ARGUMENTS` substitution. The sync lives in
|
|
85
|
+
`setup.ts` alongside the existing installers and follows their patterns:
|
|
86
|
+
idempotent content-compare like `installSkills`, both global and local
|
|
87
|
+
scopes, reported in the setup output next to the skills report.
|
|
88
|
+
(Deviation from the draft below: no `checkSetup` entry. That function
|
|
89
|
+
answers one question - "is thatch set up at all" - and command files
|
|
90
|
+
self-heal on every setup run, so flagging their absence as a startup
|
|
91
|
+
warning would be noise.) Differences: no plugin, so no
|
|
80
92
|
`command.execute.before` arming -- greenlight-token wrap-up commands do not
|
|
81
|
-
ship to Claude Code; and tool names use the `mcp__thatch__` prefix.
|
|
93
|
+
ship to Claude Code; and tool names use the `mcp__thatch__` prefix. MCP
|
|
94
|
+
hosts do have hooks now (session-start reminders, chat-wake stop hooks), so
|
|
95
|
+
hook wiring is not the barrier; the missing piece is only the plugin's TUI
|
|
96
|
+
control routes.
|
|
82
97
|
- **Cursor**: no file-based commands. The stdio MCP server (`mcp.ts`) gains
|
|
83
98
|
MCP Prompts support (`prompts/list`, `prompts/get`), which Cursor surfaces
|
|
84
|
-
as slash commands.
|
|
99
|
+
as slash commands. The dispatch switch currently handles `initialize`,
|
|
100
|
+
`tools/list`, `tools/call`, and `ping`; prompts get a `compileTools`-style
|
|
101
|
+
`compilePrompts()` and two new cases. Prompt arguments use MCP's
|
|
85
102
|
`{{argument}}`-style templating, so each action core gets an MCP-prompt
|
|
86
103
|
rendering alongside the markdown rendering.
|
|
87
104
|
|
|
88
105
|
README gains "What works in Claude Code" and "What works in Cursor" sections
|
|
89
|
-
listing per-host support without restating the shared docs.
|
|
106
|
+
listing per-host support without restating the shared docs. The parity matrix
|
|
107
|
+
in `docs/dev/mcp-parity.md` gets an Actions row next to the existing
|
|
108
|
+
Wrap-up commands row.
|
|
90
109
|
|
|
91
110
|
## Session ID plumbing
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
Since the plan was drafted, `HostToolContext` (tool-defs.ts) landed: every
|
|
113
|
+
tool's `execute` receives the invoking session's ID as a third parameter on
|
|
114
|
+
the opencode path, and the watch tools already consume it. `extraction_done`
|
|
115
|
+
also already has the optional `session_id` with the omit-vs-parent rule in
|
|
116
|
+
its description. The remaining work is narrower than first planned:
|
|
117
|
+
|
|
118
|
+
- `get_extraction_payload`: `session_id` becomes optional. `execute` uses
|
|
119
|
+
`args.session_id ?? host?.sessionID`. On the opencode path the wrapper
|
|
120
|
+
supplies the invoking session, so omitted means "this session."
|
|
121
|
+
- MCP path: the host context is omitted (no session concept), so an omitted
|
|
122
|
+
`session_id` returns a clear error telling the model to pass the parent
|
|
123
|
+
session's ID from the extraction nudge.
|
|
95
124
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
stays required. The tool description and docs state this plainly.
|
|
125
|
+
One wrinkle to document: the zod arg shape is shared between hosts, so
|
|
126
|
+
optionality is global -- you cannot make the arg required on MCP and optional
|
|
127
|
+
on opencode at the schema level. Enforcement happens in `execute` per host,
|
|
128
|
+
and the arg description must carry the rule so the model self-corrects.
|
|
101
129
|
|
|
102
130
|
The rule the LLM must apply, stated everywhere the tools are described:
|
|
103
131
|
|
|
@@ -105,10 +133,10 @@ The rule the LLM must apply, stated everywhere the tools are described:
|
|
|
105
133
|
> parent session's `session_id` when running inside a sub-agent that was
|
|
106
134
|
> dispatched to process another session's extraction queue.
|
|
107
135
|
|
|
108
|
-
This rule appears in: the
|
|
136
|
+
This rule appears in: the arg descriptions in `tool-defs.ts`, the extraction
|
|
109
137
|
prompt core (used by both nudge and action envelopes), the fact-extractor
|
|
110
|
-
skill, and the dev docs. Tests cover both paths: omit-resolves-to-current
|
|
111
|
-
explicit-parent-ID (the existing sub-agent drain bug's regression path).
|
|
138
|
+
skill, and the dev docs. Tests cover both paths: omit-resolves-to-current
|
|
139
|
+
and explicit-parent-ID (the existing sub-agent drain bug's regression path).
|
|
112
140
|
|
|
113
141
|
## Decisions
|
|
114
142
|
|
|
@@ -127,18 +155,25 @@ explicit-parent-ID (the existing sub-agent drain bug's regression path).
|
|
|
127
155
|
- `tests/commands.test.ts` (or the existing plugin tests): command-file
|
|
128
156
|
content assertions per host, including that each action's markdown contains
|
|
129
157
|
its core text verbatim.
|
|
158
|
+
- Claude setup tests: command sync writes, updates only on content change,
|
|
159
|
+
and `checkSetup` flags a missing or stale command file.
|
|
130
160
|
- MCP tests: `prompts/list` returns every action; `prompts/get` interpolates
|
|
131
|
-
arguments
|
|
132
|
-
-
|
|
133
|
-
|
|
161
|
+
arguments; an omitted-session-ID `get_extraction_payload` call errors with
|
|
162
|
+
the pass-the-parent-ID message (no host context on the MCP path).
|
|
163
|
+
- Plugin tests: `get_extraction_payload` without `session_id` resolves via
|
|
164
|
+
the host context to the invoking session; with an explicit parent ID,
|
|
165
|
+
drains the parent's queue (the existing sub-agent drain bug's regression
|
|
166
|
+
path).
|
|
134
167
|
- Parity guard: a test asserts every opencode command def has a
|
|
135
168
|
corresponding MCP prompt (and vice versa) except the documented exclusions
|
|
136
169
|
(wrap-up greenlight commands on non-opencode hosts).
|
|
137
170
|
|
|
138
171
|
## Docs
|
|
139
172
|
|
|
140
|
-
- `docs/dev/commands.md` (two-tier
|
|
173
|
+
- `docs/dev/features/commands.md` and `docs/user/commands.md` (two-tier):
|
|
141
174
|
action list, host parity matrix, session ID rule.
|
|
175
|
+
- `docs/dev/mcp-parity.md`: Actions row in the parity matrix; revisit the
|
|
176
|
+
Wrap-up commands row if greenlight arming ever lands on MCP hosts.
|
|
142
177
|
- README: "What works in Claude Code" / "What works in Cursor" sections.
|
|
143
178
|
- QA use cases: one auto use case for `/thatch/defrag` end-to-end, one for
|
|
144
179
|
`/thatch/extract` with omitted session ID.
|
package/docs/user/README.md
CHANGED
|
@@ -19,6 +19,7 @@ This README is the overview. Each feature has its own guide:
|
|
|
19
19
|
- [default-behaviors.md](default-behaviors.md): what ships automatically
|
|
20
20
|
- [hygiene.md](hygiene.md): store maintenance signals
|
|
21
21
|
- [deduplication.md](deduplication.md): duplicate detection and resolution
|
|
22
|
+
- [commands.md](commands.md): `/thatch/*` slash commands (on-demand actions and wrap-ups)
|
|
22
23
|
- [skills.md](skills.md): structured workflow skills
|
|
23
24
|
- [code-review.md](code-review.md): multi-agent code review pipeline
|
|
24
25
|
- [setup.md](setup.md): installation and configuration
|
|
@@ -242,7 +243,10 @@ Beyond the tools, thatch hooks into opencode itself:
|
|
|
242
243
|
agent listed, and you re-run the command once they're handled. Text typed
|
|
243
244
|
after the command reaches the agent first, labeled as a `User Message`
|
|
244
245
|
section ahead of the checklist (say goodbye or hand off context:
|
|
245
|
-
`/thatch/exit Good work - see you tomorrow`).
|
|
246
|
+
`/thatch/exit Good work - see you tomorrow`). Alongside the wrap-ups,
|
|
247
|
+
on-demand actions (`/thatch/defrag`, `/thatch/extract`, `/thatch/hygiene`,
|
|
248
|
+
`/thatch/reflect`) run the memory-maintenance behaviors whenever you want,
|
|
249
|
+
not just when a nudge fires -- see [commands.md](commands.md).
|
|
246
250
|
A greenlit `/thatch/exit` also unregisters the session from the chat
|
|
247
251
|
directory, so other sessions stop sending it mail. The commands
|
|
248
252
|
self-install into `~/.config/opencode/command/thatch/` at plugin startup;
|
package/docs/user/cli.md
CHANGED
|
@@ -46,6 +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 list --stale all # also show stale sessions older than a day
|
|
49
50
|
thatch chat tail # follow sent and read events, live (JSONL)
|
|
50
51
|
thatch chat tail --once # print the last 20 messages and exit
|
|
51
52
|
thatch chat tail --once --limit 50 # a longer backlog (or --limit all)
|
|
@@ -59,9 +60,15 @@ Stale, each as aligned columns under a header row: how long since its
|
|
|
59
60
|
last heartbeat, its status (`fresh` or `stale` for opencode sessions;
|
|
60
61
|
`active` or `idle` for Claude Code and Cursor sessions, which only beat
|
|
61
62
|
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?"
|
|
63
|
+
the fastest way to answer "which session should I talk to about X?"
|
|
64
|
+
Rows are grouped by project (alphabetical; sessions with no project
|
|
65
|
+
last), most recently seen first within each project. A
|
|
63
66
|
stale session has missed two heartbeats, about a minute: the opencode
|
|
64
|
-
process hosting it has probably stopped.
|
|
67
|
+
process hosting it has probably stopped. The stale section shows rows
|
|
68
|
+
up to one day old by default; older ones collapse into a hidden-count
|
|
69
|
+
note so long-dead sessions cannot bury the fresh signal. Pass
|
|
70
|
+
`--stale N` (days) to widen the window or `--stale all` to see every
|
|
71
|
+
stale row. Color is added only when the
|
|
65
72
|
output is a terminal; a pipe prints plain aligned text.
|
|
66
73
|
|
|
67
74
|
`chat tail` prints the conversation as a JSONL event log: one JSON
|
|
@@ -160,7 +167,9 @@ thatch setup --claude [--cursor] [--global] # install config + hooks + skills
|
|
|
160
167
|
|
|
161
168
|
## Limitations
|
|
162
169
|
|
|
163
|
-
-
|
|
170
|
+
- For non-Nix installs, `bun` must be installed and on PATH. Thatch does
|
|
171
|
+
not bundle its own runtime (the Nix package bundles it; see the
|
|
172
|
+
README's Nix section).
|
|
164
173
|
- `search` is limited to 10 results. There is no pagination.
|
|
165
174
|
- `prime` requires an external CLI (`opencode`, `agent`, or `claude`)
|
|
166
175
|
on PATH. If none is found, it exits with an error.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Slash commands
|
|
2
|
+
|
|
3
|
+
Thatch ships `/thatch/*` commands you run yourself, on top of the nudges the
|
|
4
|
+
plugin fires on its own schedule. Nudges and commands share the same
|
|
5
|
+
instruction text, so a behavior works the same whether it fires automatically
|
|
6
|
+
or you invoke it.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
- `/thatch/defrag` — find duplicate-candidate memory pairs and clusters,
|
|
11
|
+
classify each one, consolidate duplicates, and mark the rest checked.
|
|
12
|
+
Text after the command narrows the scope (for example, a store name).
|
|
13
|
+
- `/thatch/extract` — drain the extraction queue now, instead of waiting
|
|
14
|
+
for the next nudge or idle-time extraction. opencode only.
|
|
15
|
+
- `/thatch/hygiene` — run the hygiene report and act on it: stale
|
|
16
|
+
memories, memories scoped to deleted branches, and pending dedup pairs.
|
|
17
|
+
The agent asks before forgetting anything that may still matter.
|
|
18
|
+
- `/thatch/reflect` — run the session-reflection skill to persist what the
|
|
19
|
+
current session learned.
|
|
20
|
+
- `/thatch/compact` and `/thatch/exit` — the wrap-up commands. They run a
|
|
21
|
+
pre-flight checklist (flush extraction, finish promised memory writes,
|
|
22
|
+
surface loose ends) before a compaction or an exit, gated by a greenlight
|
|
23
|
+
token. opencode only; the greenlight mechanics are described in the
|
|
24
|
+
[user guide overview](README.md).
|
|
25
|
+
|
|
26
|
+
## Host support
|
|
27
|
+
|
|
28
|
+
| Command | opencode | Claude Code | Cursor |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `/thatch/defrag` | yes | yes | yes (as an MCP prompt) |
|
|
31
|
+
| `/thatch/extract` | yes | no | no |
|
|
32
|
+
| `/thatch/hygiene` | yes | yes | yes (as an MCP prompt) |
|
|
33
|
+
| `/thatch/reflect` | yes | yes | yes (as an MCP prompt) |
|
|
34
|
+
| `/thatch/compact`, `/thatch/exit` | yes | no | no |
|
|
35
|
+
|
|
36
|
+
Why the gaps: `/thatch/extract` needs the invoking session's ID, which only
|
|
37
|
+
the opencode plugin can supply. The wrap-up commands trigger TUI actions
|
|
38
|
+
through the plugin. Cursor has no file-based commands; its prompts come from
|
|
39
|
+
the MCP server (`prompts/list` and `prompts/get`).
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
- **opencode**: installed by the plugin on every load.
|
|
44
|
+
- **Claude Code**: installed by `thatch setup --claude` into the same scope
|
|
45
|
+
as the skills (`.claude/commands/thatch/` project-local, or the Claude
|
|
46
|
+
config dir for `--global`). Re-run setup to refresh after an upgrade.
|
|
47
|
+
|
|
48
|
+
Tool names inside the command text follow each host's spelling
|
|
49
|
+
(`thatch_memory_remember` on opencode, `mcp__thatch__memory_remember` on
|
|
50
|
+
Claude Code and Cursor).
|
|
@@ -8,8 +8,10 @@ between tabs all afternoon.
|
|
|
8
8
|
|
|
9
9
|
## What it does
|
|
10
10
|
|
|
11
|
-
opencode sessions join the directory automatically,
|
|
12
|
-
|
|
11
|
+
opencode sessions join the directory automatically, the moment their
|
|
12
|
+
first user message arrives (before the assistant responds), and a session
|
|
13
|
+
resumed with `opencode -c` or `opencode -s <id>` is back in the directory
|
|
14
|
+
at harness start with its old name. Names are assigned by thatch, never chosen: a name drawn from a
|
|
13
15
|
built-in pool plus a counter (`al-go-rithm-00001`). The counter only ever
|
|
14
16
|
increments, so a name is
|
|
15
17
|
minted exactly once per machine - pruning an old session never reissues its
|
|
@@ -60,11 +62,13 @@ toggle reveals those output blocks if you want them.)
|
|
|
60
62
|
## How to use it
|
|
61
63
|
|
|
62
64
|
With auto-registration you usually do nothing: a session joins the
|
|
63
|
-
directory when
|
|
64
|
-
`opencode -s <id>` is registered again the moment
|
|
65
|
-
same session id, same name, reclaimed. The agent
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
directory when its first user message arrives, and a session continued
|
|
66
|
+
with `opencode -c` or `opencode -s <id>` is registered again the moment
|
|
67
|
+
its harness starts - same session id, same name, reclaimed. The agent
|
|
68
|
+
gets a quiet toast with
|
|
69
|
+
its assigned name when it first registers, and a resumed session
|
|
70
|
+
announces its reclaimed name on its first prompt. To introduce two
|
|
71
|
+
sessions, just tell each one who to talk to:
|
|
68
72
|
|
|
69
73
|
> Ask the other session whether the release has shipped; answer its
|
|
70
74
|
> questions directly.
|
package/docs/user/setup.md
CHANGED
|
@@ -90,6 +90,7 @@ and `--json` output for hook commands.
|
|
|
90
90
|
| `THATCH_DB_PATH` | `~/.config/thatch/thatch.db` | SQLite database path |
|
|
91
91
|
| `THATCH_MODEL` | `Xenova/bge-small-en-v1.5` | Hugging Face model name for embeddings |
|
|
92
92
|
| `THATCH_EMBEDDING_BACKEND` | `wasm` | Set to `native` to run embeddings on onnxruntime-node (NAPI) instead of the wasm runtime |
|
|
93
|
+
| `THATCH_MODEL_CACHE` | platform per-user cache dir | Where downloaded model files are stored |
|
|
93
94
|
| `THATCH_RECALL_THRESHOLD` | `0.55` | Cosine threshold for recall nudge |
|
|
94
95
|
| `THATCH_PREDICTION_THRESHOLD` | `0.60` | Cosine threshold for prediction auto-fire |
|
|
95
96
|
| `THATCH_BEHAVIOR_THRESHOLD` | `0.60` | Cosine threshold for behavior auto-fire |
|
|
@@ -110,10 +111,15 @@ instructions are deployed without requiring you to manually re-run
|
|
|
110
111
|
|
|
111
112
|
## Limitations
|
|
112
113
|
|
|
113
|
-
- `bun` must be installed and on PATH. Thatch does
|
|
114
|
-
own runtime
|
|
114
|
+
- For non-Nix installs, `bun` must be installed and on PATH. Thatch does
|
|
115
|
+
not bundle its own runtime (the Nix package bundles it; see the
|
|
116
|
+
README's Nix section).
|
|
115
117
|
- The embedding model (~34 MB) is downloaded once on first use and
|
|
116
|
-
cached in
|
|
118
|
+
cached in the platform's per-user cache dir
|
|
119
|
+
(`~/Library/Caches/thatch/models` on macOS (or
|
|
120
|
+
`$XDG_CACHE_HOME/thatch/models` when `XDG_CACHE_HOME` is set),
|
|
121
|
+
`$XDG_CACHE_HOME/thatch/models` elsewhere). Override with
|
|
122
|
+
`THATCH_MODEL_CACHE`.
|
|
117
123
|
- There is no web UI or dashboard. All interaction is through the
|
|
118
124
|
agent's tool calls and the `thatch` CLI.
|
|
119
125
|
- The MCP server is a long-lived process that keeps the embedding model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
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.ts
CHANGED
|
@@ -304,17 +304,45 @@ export function chatLiveness(row: ChatSessionRow, now = Date.now()): "fresh" | "
|
|
|
304
304
|
return stale ? "stale" : "fresh";
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/** Orders roster rows for display: project first (so a shared-tree reader
|
|
308
|
+
* sees each repo's sessions together; rows with no project sort last),
|
|
309
|
+
* then by heartbeat, most recently seen first, so the recoverable rows
|
|
310
|
+
* lead and the long-dead trail at the bottom. Pure ordering - it never
|
|
311
|
+
* drops rows; the stale display cap is splitChatRoster's job. */
|
|
312
|
+
export function sortChatRoster(rows: ChatSessionRow[]): ChatSessionRow[] {
|
|
313
|
+
return [...rows].sort((a, b) => {
|
|
314
|
+
const projectA = a.project ?? "\uffff";
|
|
315
|
+
const projectB = b.project ?? "\uffff";
|
|
316
|
+
if (projectA !== projectB) return projectA.localeCompare(projectB);
|
|
317
|
+
return Date.parse(b.last_seen) - Date.parse(a.last_seen);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
307
321
|
/** Splits the roster for the two-section display: Active (wake-able or
|
|
308
322
|
* reachable - fresh opencode rows and every MCP row, which read mail at
|
|
309
323
|
* their next prompt) and Stale (opencode rows whose harness has stopped
|
|
310
|
-
* reporting; mail to them waits until they resume).
|
|
311
|
-
|
|
324
|
+
* reporting; mail to them waits until they resume).
|
|
325
|
+
* staleCapMs bounds how old a stale row the roster displays, so
|
|
326
|
+
* long-dead sessions cannot bury the fresh signal; hidden rows come
|
|
327
|
+
* back only as the staleHidden count, so the CLI can say what it left
|
|
328
|
+
* out. null (the default) shows every stale row - the tool surface has
|
|
329
|
+
* no cap, only the CLI applies one. */
|
|
330
|
+
export function splitChatRoster(rows: ChatSessionRow[], now = Date.now(), staleCapMs: number | null = null): { active: ChatSessionRow[]; stale: ChatSessionRow[]; staleHidden: number } {
|
|
312
331
|
const active: ChatSessionRow[] = [];
|
|
313
332
|
const stale: ChatSessionRow[] = [];
|
|
333
|
+
let staleHidden = 0;
|
|
314
334
|
for (const row of rows) {
|
|
315
|
-
(chatLiveness(row, now) === "stale"
|
|
335
|
+
if (chatLiveness(row, now) === "stale") {
|
|
336
|
+
if (staleCapMs !== null && now - Date.parse(row.last_seen) > staleCapMs) {
|
|
337
|
+
staleHidden++;
|
|
338
|
+
} else {
|
|
339
|
+
stale.push(row);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
active.push(row);
|
|
343
|
+
}
|
|
316
344
|
}
|
|
317
|
-
return { active, stale };
|
|
345
|
+
return { active, stale, staleHidden };
|
|
318
346
|
}
|
|
319
347
|
|
|
320
348
|
/**
|
package/src/commands.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
defragCore,
|
|
5
|
+
extractionCore,
|
|
6
|
+
hygieneCore,
|
|
7
|
+
mcpToolName,
|
|
8
|
+
opencodeToolName,
|
|
9
|
+
reflectCore,
|
|
10
|
+
type ToolNamer,
|
|
11
|
+
} from "./prompts";
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
|
-
* Thatch's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* so
|
|
14
|
+
* Thatch's slash commands, shipped as command markdown files synced into each
|
|
15
|
+
* host's command directory:
|
|
16
|
+
*
|
|
17
|
+
* - opencode: ~/.config/opencode/command/thatch/ - synced by the plugin on
|
|
18
|
+
* every load, so template updates self-heal without a reinstall. Config is
|
|
19
|
+
* loaded before plugins during server startup, so a first-ever install
|
|
20
|
+
* becomes visible on the next start.
|
|
21
|
+
* - Claude Code: ~/.claude/commands/thatch/ (or the project's .claude/
|
|
22
|
+
* commands/thatch/) - synced by `thatch setup --claude`, which has no
|
|
23
|
+
* plugin to do it on load.
|
|
24
|
+
*
|
|
25
|
+
* Two command families live here:
|
|
26
|
+
*
|
|
27
|
+
* - Actions (/thatch/defrag, extract, hygiene, reflect): user-invoked
|
|
28
|
+
* wrappers around a behavior that the nudges otherwise only run on their
|
|
29
|
+
* own schedule. Each action body is a prompt core from src/prompts.ts, so
|
|
30
|
+
* nudge and command wording cannot drift.
|
|
31
|
+
* - Wrap-ups (/thatch/compact, exit): greenlight-token commands that need
|
|
32
|
+
* the opencode plugin's TUI control routes, so they are opencode-only.
|
|
10
33
|
*/
|
|
11
34
|
|
|
12
35
|
/** Greenlight token ending /thatch/compact's assistant response when compaction is safe. */
|
|
@@ -32,6 +55,71 @@ $ARGUMENTS
|
|
|
32
55
|
|
|
33
56
|
(That section carries the text typed after the command. When it is empty, treat the user message as n/a - the command was run bare.)`;
|
|
34
57
|
|
|
58
|
+
/**
|
|
59
|
+
* One on-demand action: the command name (minus the thatch/ prefix), the
|
|
60
|
+
* description shown in the command menu, and the instruction body (a prompt
|
|
61
|
+
* core). opencodeOnly marks actions that need a session identity or other
|
|
62
|
+
* plugin-only capability; those are excluded from Claude Code command files
|
|
63
|
+
* and MCP prompts.
|
|
64
|
+
*/
|
|
65
|
+
export interface ActionDef {
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
body: string;
|
|
69
|
+
opencodeOnly?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The on-demand action set, shared by every host that renders actions. The
|
|
74
|
+
* bodies come from prompt cores so a wording change lands in the nudge and
|
|
75
|
+
* the command at once. `tool` renders tool names in the host's spelling.
|
|
76
|
+
*/
|
|
77
|
+
export function actionDefs(tool: ToolNamer): ActionDef[] {
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
name: "defrag",
|
|
81
|
+
description: "Consolidate duplicate and near-duplicate memories in the store",
|
|
82
|
+
body: defragCore(tool),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "extract",
|
|
86
|
+
description: "Drain the extraction queue now",
|
|
87
|
+
// Needs the invoking session's ID (via get_session_info, opencode-only)
|
|
88
|
+
// and the plugin's extraction payload provider.
|
|
89
|
+
opencodeOnly: true,
|
|
90
|
+
body: `First call thatch_get_session_info to learn this session's ID. Below, replace SESSION_ID with that value.
|
|
91
|
+
|
|
92
|
+
Dispatch a task with background: true and subagent_type: "general" (required for thatch tool access) with this prompt:
|
|
93
|
+
|
|
94
|
+
"${extractionCore("thatch_get_extraction_payload", "thatch_extraction_done", ' with session_id \\"SESSION_ID\\"')}"
|
|
95
|
+
|
|
96
|
+
After dispatching, call thatch_extraction_done to acknowledge.`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "hygiene",
|
|
100
|
+
description: "Tend the memory store: stale entries, orphans, pending dedup pairs",
|
|
101
|
+
body: hygieneCore(),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "reflect",
|
|
105
|
+
description: "Persist what this session learned as memories",
|
|
106
|
+
body: reflectCore(tool),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Renders one action as a full command markdown file. */
|
|
112
|
+
export function renderActionCommand(action: ActionDef): string {
|
|
113
|
+
return `---
|
|
114
|
+
description: ${action.description}
|
|
115
|
+
---
|
|
116
|
+
${userMessageSection}
|
|
117
|
+
|
|
118
|
+
# ${action.name.charAt(0).toUpperCase() + action.name.slice(1)}
|
|
119
|
+
|
|
120
|
+
${action.body}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
35
123
|
const COMPACT_TEMPLATE = `---
|
|
36
124
|
description: Flush thatch persistence, check for loose ends, then compact if clear
|
|
37
125
|
---
|
|
@@ -67,29 +155,40 @@ ${EXIT_READY_TOKEN}
|
|
|
67
155
|
|
|
68
156
|
If anything is outstanding, list the items concisely so the user can address them, and do NOT include the token. opencode will only exit when the token is present.`;
|
|
69
157
|
|
|
70
|
-
/** One
|
|
158
|
+
/** One command: the command name (minus the thatch/ prefix) and its full markdown file content. */
|
|
71
159
|
export interface CommandDef {
|
|
72
160
|
name: string;
|
|
73
161
|
content: string;
|
|
74
162
|
}
|
|
75
163
|
|
|
76
|
-
/** The wrap-
|
|
164
|
+
/** The opencode command set: wrap-ups plus every action, as full file contents. */
|
|
77
165
|
export function opencodeCommandDefs(): CommandDef[] {
|
|
78
166
|
return [
|
|
79
167
|
{ name: "compact", content: COMPACT_TEMPLATE },
|
|
80
168
|
{ name: "exit", content: EXIT_TEMPLATE },
|
|
169
|
+
...actionDefs(opencodeToolName).map((a) => ({ name: a.name, content: renderActionCommand(a) })),
|
|
81
170
|
];
|
|
82
171
|
}
|
|
83
172
|
|
|
84
173
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
174
|
+
* The Claude Code command set: the actions minus the opencode-only ones.
|
|
175
|
+
* The wrap-up commands are excluded too - their greenlight check needs the
|
|
176
|
+
* plugin's command.execute.before arming and TUI control routes.
|
|
88
177
|
*/
|
|
89
|
-
export function
|
|
90
|
-
|
|
178
|
+
export function claudeCommandDefs(): CommandDef[] {
|
|
179
|
+
return actionDefs(mcpToolName)
|
|
180
|
+
.filter((a) => !a.opencodeOnly)
|
|
181
|
+
.map((a) => ({ name: a.name, content: renderActionCommand(a) }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Sync command files into dir. Idempotent: each file is written only when
|
|
186
|
+
* its on-disk content differs. Returns the paths that were written (empty
|
|
187
|
+
* when everything was current).
|
|
188
|
+
*/
|
|
189
|
+
function syncCommandFiles(dir: string, defs: CommandDef[]): string[] {
|
|
91
190
|
const written: string[] = [];
|
|
92
|
-
for (const def of
|
|
191
|
+
for (const def of defs) {
|
|
93
192
|
const path = join(dir, `${def.name}.md`);
|
|
94
193
|
let current: string | null = null;
|
|
95
194
|
try {
|
|
@@ -105,3 +204,13 @@ export function installOpencodeCommands(configHome: string): string[] {
|
|
|
105
204
|
}
|
|
106
205
|
return written;
|
|
107
206
|
}
|
|
207
|
+
|
|
208
|
+
/** Sync opencode's commands into <configHome>/opencode/command/thatch/. */
|
|
209
|
+
export function installOpencodeCommands(configHome: string): string[] {
|
|
210
|
+
return syncCommandFiles(join(configHome, "opencode", "command", "thatch"), opencodeCommandDefs());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Sync Claude Code's commands into <claudeDir>/commands/thatch/ (claudeDir is ~/.claude or <project>/.claude). */
|
|
214
|
+
export function installClaudeCommands(claudeDir: string): string[] {
|
|
215
|
+
return syncCommandFiles(join(claudeDir, "commands", "thatch"), claudeCommandDefs());
|
|
216
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
2
4
|
import { blobToVector, cosineSimilarity } from "./vector-math";
|
|
3
5
|
import {
|
|
4
6
|
PredictionEngine,
|
|
@@ -58,6 +60,13 @@ export class ThatchDB {
|
|
|
58
60
|
#chat: ChatStore;
|
|
59
61
|
|
|
60
62
|
constructor(path: string) {
|
|
63
|
+
// bun:sqlite creates the file but not its parent directory. On a fresh
|
|
64
|
+
// machine the default path (~/.config/thatch/thatch.db) has no dir yet, so
|
|
65
|
+
// create it here - otherwise every command fails with SQLITE_CANTOPEN.
|
|
66
|
+
// `:memory:` and other special paths have no meaningful parent to create.
|
|
67
|
+
if (path !== ":memory:" && !path.startsWith("file:")) {
|
|
68
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
69
|
+
}
|
|
61
70
|
this.#db = new Database(path, { create: true });
|
|
62
71
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
63
72
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
package/src/embeddings.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
// BGE-small-en-v1.5 requires a query prefix for asymmetric search.
|
|
2
5
|
// Passage (memory content) gets no prefix - the model was trained
|
|
3
6
|
// to encode passages without instruction.
|
|
4
7
|
const QUERY_PREFIX =
|
|
5
8
|
"Represent this sentence for searching relevant passages: ";
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Writable directory for the downloaded model files.
|
|
12
|
+
*
|
|
13
|
+
* transformers.js defaults its filesystem cache to a `.cache` folder *inside*
|
|
14
|
+
* its own package directory (`node_modules/@huggingface/transformers/.cache`).
|
|
15
|
+
* That breaks any read-only install - a global `bun install -g`, and Nix store
|
|
16
|
+
* paths in particular - and gets wiped on every reinstall. Anchor the cache in
|
|
17
|
+
* the platform's per-user cache location instead so it survives upgrades and
|
|
18
|
+
* works from an immutable store path.
|
|
19
|
+
*
|
|
20
|
+
* THATCH_MODEL_CACHE explicit override (highest priority)
|
|
21
|
+
* XDG_CACHE_HOME explicit base dir -> $XDG_CACHE_HOME/thatch/models
|
|
22
|
+
* macOS ~/Library/Caches/thatch/models
|
|
23
|
+
* Windows %LOCALAPPDATA%\thatch\models
|
|
24
|
+
* Linux / other ~/.cache/thatch/models
|
|
25
|
+
*
|
|
26
|
+
* `platform` and `home` are injectable for testing; they default to the real
|
|
27
|
+
* runtime values.
|
|
28
|
+
*/
|
|
29
|
+
export function modelCacheDir(
|
|
30
|
+
platform: NodeJS.Platform = process.platform,
|
|
31
|
+
home: string = homedir(),
|
|
32
|
+
): string {
|
|
33
|
+
const env = process.env;
|
|
34
|
+
if (env.THATCH_MODEL_CACHE) return env.THATCH_MODEL_CACHE;
|
|
35
|
+
// Respect an explicit XDG override on any platform (some macOS users set it).
|
|
36
|
+
if (env.XDG_CACHE_HOME) return join(env.XDG_CACHE_HOME, "thatch", "models");
|
|
37
|
+
const base =
|
|
38
|
+
platform === "darwin"
|
|
39
|
+
? join(home, "Library", "Caches")
|
|
40
|
+
: platform === "win32"
|
|
41
|
+
? (env.LOCALAPPDATA ?? join(home, "AppData", "Local"))
|
|
42
|
+
: join(home, ".cache");
|
|
43
|
+
return join(base, "thatch", "models");
|
|
44
|
+
}
|
|
45
|
+
|
|
7
46
|
/**
|
|
8
47
|
* Interface for an embedding model so tests can supply a mock.
|
|
9
48
|
* `name` is the tag stored alongside each entry so a future reader can tell
|
|
@@ -90,7 +129,9 @@ export function resetBackendForTests(): void {
|
|
|
90
129
|
|
|
91
130
|
const defaultPipelineFactory: PipelineFactory = async (modelName) => {
|
|
92
131
|
const mode = await configureBackend();
|
|
93
|
-
const { pipeline } = await import("@huggingface/transformers");
|
|
132
|
+
const { pipeline, env } = await import("@huggingface/transformers");
|
|
133
|
+
// Redirect the model cache out of the (possibly read-only) package tree.
|
|
134
|
+
env.cacheDir = modelCacheDir();
|
|
94
135
|
if (mode === "native") {
|
|
95
136
|
return pipeline("feature-extraction", modelName);
|
|
96
137
|
}
|