@jeffober/thatch 0.1.40 → 0.1.42
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 +27 -10
- package/artifacts/skills/thatch-code-review.md +4 -4
- package/artifacts/skills/thatch-review-synthesizer.md +8 -8
- package/artifacts/skills/{thatch-review-mentorship.md → thatch-review-technique.md} +9 -10
- package/bin/release +4 -1
- package/bin/thatch +318 -18
- package/docs/dev/README.md +29 -15
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/cli.md +33 -4
- package/docs/dev/features/cross-session-chat.md +315 -0
- package/docs/dev/features/database.md +52 -4
- package/docs/dev/features/multi-host.md +18 -9
- package/docs/dev/features/setup.md +12 -5
- package/docs/dev/features/watchers.md +113 -23
- package/docs/dev/gotchas.md +7 -4
- package/docs/dev/mcp-parity.md +2 -2
- package/docs/dev/setup-and-hooks.md +5 -4
- package/docs/dev/skills.md +5 -4
- package/docs/user/README.md +25 -6
- package/docs/user/cli.md +78 -0
- package/docs/user/code-review.md +1 -1
- package/docs/user/cross-session-chat.md +150 -0
- package/docs/user/setup.md +16 -2
- package/docs/user/skills.md +7 -6
- package/docs/user/watchers.md +61 -16
- package/package.json +2 -1
- package/src/chat-names.ts +120 -0
- package/src/chat.ts +1210 -0
- package/src/config.ts +34 -1
- package/src/db.ts +227 -0
- package/src/index.ts +188 -17
- package/src/prompts.ts +218 -29
- package/src/setup.ts +83 -29
- package/src/skills.ts +47 -17
- package/src/tool-defs.ts +574 -31
- package/src/tools.ts +2 -0
- package/src/watchers.ts +358 -23
package/bin/thatch
CHANGED
|
@@ -10,6 +10,8 @@ import { hygieneReport } from "../src/hygiene";
|
|
|
10
10
|
import { claudeSessionStartReminder } from "../src/prompts";
|
|
11
11
|
import { setupClaudeCode, setupCursor } from "../src/setup";
|
|
12
12
|
import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscriptEntries, partToFullJson, messageToFullJson, type SessionPart } from "../src/session-db";
|
|
13
|
+
import { chatTailDiff, chatTailBacklog, filterChatTailRows, formatChatTailCard, CHAT_TAIL_SEPARATOR, CHAT_TAIL_DEFAULT_LIMIT, parseChatTimeBound, isStale, CHAT_STALE_MINUTES, mcpSessionID, type ChatTailFilter } from "../src/chat";
|
|
14
|
+
import { chatEnabled, loadConfig } from "../src/config";
|
|
13
15
|
|
|
14
16
|
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
15
17
|
const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
|
|
@@ -33,7 +35,7 @@ function usage(): never {
|
|
|
33
35
|
thatch buffer-tool Append a single postToolUse interaction to the queue (Cursor hook only)
|
|
34
36
|
thatch flush-tools [--json] Peek the queue + print an extraction nudge (hook only)
|
|
35
37
|
thatch flush-predictions [--json] Query predictions for the prompt (hook only)
|
|
36
|
-
thatch setup --claude [--cursor] [--global]
|
|
38
|
+
thatch setup --claude [--cursor] [--local|--global]
|
|
37
39
|
Install config + instructions + hooks + skills
|
|
38
40
|
thatch session list -s <id> [--after T] [--before T]
|
|
39
41
|
JSONL timeline of a session's parts (one line per part)
|
|
@@ -44,6 +46,18 @@ function usage(): never {
|
|
|
44
46
|
Full conversation as OpenAI chat-completions JSONL
|
|
45
47
|
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
46
48
|
JSONL of matching parts across sessions (decoded content)
|
|
49
|
+
thatch chat list Registered chat sessions: name, age, project, topic
|
|
50
|
+
thatch chat tail [--once] [--limit N|all] [--match RE]
|
|
51
|
+
[--from NAME] [--to NAME] [--since DT] [--until DT]
|
|
52
|
+
Follow cross-session chat: sent and read
|
|
53
|
+
events. The backlog shows the last
|
|
54
|
+
${CHAT_TAIL_DEFAULT_LIMIT} messages
|
|
55
|
+
(--limit N or --limit all to override).
|
|
56
|
+
--match filters message bodies
|
|
57
|
+
(repeatable, ANDed); --from/--to filter
|
|
58
|
+
participant names; --since/--until bound
|
|
59
|
+
the window ("YYYY-MM-DD [HH:MM]" local
|
|
60
|
+
time). Filters apply to follow mode too.
|
|
47
61
|
|
|
48
62
|
Stores default to the repo detected from git remote. Use "global" for the
|
|
49
63
|
global store. Use "all" with search to search project + global together.`);
|
|
@@ -58,13 +72,75 @@ async function defaultStore(): Promise<string> {
|
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Builds the chat-mail hook line for the given project. With a hook
|
|
77
|
+
* session ID (Claude Code/Cursor pass one in the hook's stdin JSON), it
|
|
78
|
+
* first ensures the caller is registered - the host's assigned identity -
|
|
79
|
+
* and prints that identity plus the caller's own unread count, so the
|
|
80
|
+
* model learns who it is on every prompt without a tool call. Without a
|
|
81
|
+
* session ID it keeps the old shape: a line only when pending mail exists
|
|
82
|
+
* for registered sessions in the project, naming the addressees.
|
|
83
|
+
*/
|
|
84
|
+
async function chatHookLine(hookSessionID?: string): Promise<string> {
|
|
85
|
+
// Disabled chat prints nothing: the hook line must not advertise a
|
|
86
|
+
// feature the user turned off (the tools refuse, the poller never
|
|
87
|
+
// starts, the prompt section omits chat).
|
|
88
|
+
if (!chatEnabled(loadConfig(dbPath).config)) return "";
|
|
89
|
+
let repo: string | null = null;
|
|
90
|
+
try {
|
|
91
|
+
repo = await detectRepo();
|
|
92
|
+
} catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hookSessionID) {
|
|
97
|
+
// Ensure-register: idempotent, so every prompt re-touches liveness and
|
|
98
|
+
// the first prompt of a conversation mints the assigned name. This is
|
|
99
|
+
// the MCP identity anchor - the session ID is stable per conversation,
|
|
100
|
+
// so re-registering finds the same row and name.
|
|
101
|
+
const sessionID = mcpSessionID(hookSessionID);
|
|
102
|
+
// The leave tombstone blocks this ensure: chat_unregister must outlive
|
|
103
|
+
// the turn that issued it, on MCP hosts as on opencode. The line is
|
|
104
|
+
// the standing signal - it repeats every prompt until the model
|
|
105
|
+
// rejoins, and it says plainly that rejoining mints a fresh identity,
|
|
106
|
+
// because the old one is closed for good (assigned names are never
|
|
107
|
+
// reused, so a tombstoned identity cannot be resurrected by a later
|
|
108
|
+
// register).
|
|
109
|
+
if (db.hasChatLeaveTombstone(sessionID) && !db.chatLeaveSuperseded(sessionID, repo)) {
|
|
110
|
+
return `[thatch] chat: this conversation left the chat directory (chat_unregister) and will not receive mail. If the user asks to rejoin, call thatch_chat_register with no arguments - that mints a fresh identity; the old one stays closed.`;
|
|
111
|
+
}
|
|
112
|
+
db.registerChatSession(sessionID, repo, null, "mcp");
|
|
113
|
+
const me = db.findChatSession(sessionID);
|
|
114
|
+
if (!me) return "";
|
|
115
|
+
const mine = db.unreadChatCount(sessionID);
|
|
116
|
+
const pendingOthers = db
|
|
117
|
+
.chatPendingByProject(repo)
|
|
118
|
+
.filter((p) => p.name !== me.name);
|
|
119
|
+
const others = pendingOthers.length > 0
|
|
120
|
+
? ` Others in this project with unread mail: ${pendingOthers.map((p) => `${p.name} (${p.pending})`).join(", ")}.`
|
|
121
|
+
: "";
|
|
122
|
+
const mailbox = mine > 0 ? `${mine} unread message(s) for you` : "no unread mail for you";
|
|
123
|
+
return `[thatch] chat: you are ${me.name} (${mailbox}).${others}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pending = db.chatPendingByProject(repo);
|
|
127
|
+
if (pending.length === 0) return "";
|
|
128
|
+
const parts = pending.map((p) => `${p.name} (${p.pending})`);
|
|
129
|
+
return `[thatch] chat: unread message(s) for ${parts.join(", ")} - the addressee should call chat_read`;
|
|
130
|
+
}
|
|
131
|
+
|
|
61
132
|
/**
|
|
62
133
|
* Emits the nudge output, prepending a version warning if present. The
|
|
63
134
|
* warning goes first so the LLM sees it before the nudge text. Used by
|
|
64
135
|
* flush-tools to surface version skew and update-available notices on
|
|
65
|
-
* every UserPromptSubmit, regardless of which tier fired.
|
|
136
|
+
* every UserPromptSubmit, regardless of which tier fired. The chat hook
|
|
137
|
+
* line, when present, is appended last (the identity/mail check is the
|
|
138
|
+
* last thing the model reads before the nudge body).
|
|
66
139
|
*/
|
|
67
|
-
function emit(output: string, versionWarning: string | null, isJson: boolean): void {
|
|
140
|
+
function emit(output: string, versionWarning: string | null, isJson: boolean, chatLine = ""): void {
|
|
141
|
+
if (chatLine) {
|
|
142
|
+
output = `${output}\n\n${chatLine}`;
|
|
143
|
+
}
|
|
68
144
|
if (versionWarning) {
|
|
69
145
|
output = `[thatch] ${versionWarning}\n\n${output}`;
|
|
70
146
|
}
|
|
@@ -93,17 +169,42 @@ function parseTimeBound(value: string, flag: string): number {
|
|
|
93
169
|
return parsed;
|
|
94
170
|
}
|
|
95
171
|
|
|
96
|
-
/** Reads a flag's value (-flag value or --flag=value forms).
|
|
172
|
+
/** Reads a flag's value (-flag value or --flag=value forms). A flag given
|
|
173
|
+
* with no value is a typo, not a default - it dies loudly so `--until`
|
|
174
|
+
* at the end of a command line cannot silently disable the bound. */
|
|
97
175
|
function flagValue(flags: string[], ...names: string[]): string | undefined {
|
|
98
176
|
for (let i = 0; i < flags.length; i++) {
|
|
99
177
|
for (const name of names) {
|
|
100
|
-
if (flags[i] === name)
|
|
178
|
+
if (flags[i] === name) {
|
|
179
|
+
if (flags[i + 1] === undefined) {
|
|
180
|
+
console.error(`error: ${name} expects a value`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
return flags[i + 1];
|
|
184
|
+
}
|
|
101
185
|
if (flags[i]?.startsWith(`${name}=`)) return flags[i]!.slice(name.length + 1);
|
|
102
186
|
}
|
|
103
187
|
}
|
|
104
188
|
return undefined;
|
|
105
189
|
}
|
|
106
190
|
|
|
191
|
+
/** Collects every occurrence of a repeatable flag, dying on a dangling one. */
|
|
192
|
+
function flagValues(flags: string[], ...names: string[]): string[] {
|
|
193
|
+
const out: string[] = [];
|
|
194
|
+
for (let i = 0; i < flags.length; i++) {
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
if (flags[i] === name) {
|
|
197
|
+
if (flags[i + 1] === undefined) {
|
|
198
|
+
console.error(`error: ${name} expects a value`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
out.push(flags[i + 1]);
|
|
202
|
+
} else if (flags[i]?.startsWith(`${name}=`)) out.push(flags[i]!.slice(name.length + 1));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
107
208
|
function openSessionDb(): SessionDB {
|
|
108
209
|
const path = resolveOpencodeDbPath();
|
|
109
210
|
if (!path) {
|
|
@@ -219,6 +320,175 @@ async function sessionCommand(rest: string[]): Promise<void> {
|
|
|
219
320
|
}
|
|
220
321
|
}
|
|
221
322
|
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// chat subcommand group - read-only window on the cross-session chat
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
/** "2m ago", "3h ago", "5d ago" - human-readable age for the chat roster. */
|
|
328
|
+
function humanAge(iso: string, now = Date.now()): string {
|
|
329
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(iso)) / 1000));
|
|
330
|
+
if (Number.isNaN(seconds)) return "unknown age";
|
|
331
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
332
|
+
const minutes = Math.floor(seconds / 60);
|
|
333
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
334
|
+
if (minutes < 24 * 60) return `${Math.floor(minutes / 60)}h ago`;
|
|
335
|
+
return `${Math.floor(minutes / (24 * 60))}d ago`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function chatCommand(rest: string[]): Promise<void> {
|
|
339
|
+
const [sub, ...restArgs] = rest;
|
|
340
|
+
|
|
341
|
+
switch (sub) {
|
|
342
|
+
case "list": {
|
|
343
|
+
const sessions = db.listChatSessions();
|
|
344
|
+
if (sessions.length === 0) {
|
|
345
|
+
console.log("No sessions registered. Sessions join the directory automatically on their first idle (opencode), or via chat_register.");
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
for (const s of sessions) {
|
|
349
|
+
// Same staleness convention as the chat_list tool: an age past the
|
|
350
|
+
// threshold means the session's host process is probably gone.
|
|
351
|
+
const liveness = isStale(s, CHAT_STALE_MINUTES) ? "STALE" : "fresh";
|
|
352
|
+
const parts = [
|
|
353
|
+
s.name,
|
|
354
|
+
`(${humanAge(s.last_seen)})`,
|
|
355
|
+
liveness,
|
|
356
|
+
`project:${s.project ?? "unknown"}`,
|
|
357
|
+
];
|
|
358
|
+
if (s.topic) parts.push(s.topic);
|
|
359
|
+
console.log(parts.join(" "));
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case "tail": {
|
|
365
|
+
const onceOnly = restArgs.includes("--once");
|
|
366
|
+
// Parse and validate everything up front: a bad regex or date must
|
|
367
|
+
// fail before any cards print, not mid-follow.
|
|
368
|
+
const matches = flagValues(restArgs, "--match").map((pattern) => {
|
|
369
|
+
try {
|
|
370
|
+
return new RegExp(pattern);
|
|
371
|
+
} catch {
|
|
372
|
+
console.error(`error: --match is not a valid regular expression: "${pattern}"`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
const limitArg = flagValue(restArgs, "--limit");
|
|
377
|
+
let limit: number | null = CHAT_TAIL_DEFAULT_LIMIT;
|
|
378
|
+
if (limitArg !== undefined) {
|
|
379
|
+
if (limitArg === "all") limit = null;
|
|
380
|
+
else if (!/^\d+$/.test(limitArg)) {
|
|
381
|
+
console.error(`error: --limit expects a positive integer or "all", got "${limitArg}"`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
} else limit = Number(limitArg);
|
|
384
|
+
if (limit !== null && limit < 1) {
|
|
385
|
+
console.error(`error: --limit expects a positive integer or "all", got "${limitArg}"`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
let sinceMs: number | null = null;
|
|
390
|
+
let untilMs: number | null = null;
|
|
391
|
+
try {
|
|
392
|
+
const since = flagValue(restArgs, "--since");
|
|
393
|
+
const until = flagValue(restArgs, "--until");
|
|
394
|
+
if (since !== undefined) sinceMs = parseChatTimeBound(since, "--since");
|
|
395
|
+
if (until !== undefined) untilMs = parseChatTimeBound(until, "--until");
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error(`error: ${(err as Error).message}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
if (sinceMs !== null && untilMs !== null && sinceMs >= untilMs) {
|
|
401
|
+
console.error("error: --since is not before --until; the window is empty");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const fromName = flagValue(restArgs, "--from");
|
|
405
|
+
const toName = flagValue(restArgs, "--to");
|
|
406
|
+
const filter: ChatTailFilter = {
|
|
407
|
+
matches,
|
|
408
|
+
fromSubstr: fromName !== undefined ? [fromName] : [],
|
|
409
|
+
toSubstr: toName !== undefined ? [toName] : [],
|
|
410
|
+
sinceMs,
|
|
411
|
+
untilMs,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Diff state: one entry per seen message id holding its read_at, so
|
|
415
|
+
// each poll emits only new messages and newly-read rows. The diff
|
|
416
|
+
// and backlog logic lives in src/chat.ts (chatTailDiff,
|
|
417
|
+
// chatTailBacklog), unit-tested there. The backlog seeds the state
|
|
418
|
+
// from every feed row, so the limit hides cards, not history.
|
|
419
|
+
let state: Map<number, string | null>;
|
|
420
|
+
let firstFollowEvent = true;
|
|
421
|
+
|
|
422
|
+
// Follow mode holds the handle open until Ctrl-C. The finally below
|
|
423
|
+
// closes it on every exit path except the SIGINT handler's, which
|
|
424
|
+
// is why the handler closes it itself: process.exit does not unwind
|
|
425
|
+
// finally blocks (closing twice is harmless).
|
|
426
|
+
try {
|
|
427
|
+
// First pass: render the backlog as cards, then follow unless
|
|
428
|
+
// --once or the --until window has already closed (a closed
|
|
429
|
+
// window has no future events to follow). The backlog fetch gets
|
|
430
|
+
// the same friendly failure treatment as follow polls: a locked
|
|
431
|
+
// or missing database should say so, not die with a stack trace.
|
|
432
|
+
let backlog;
|
|
433
|
+
try {
|
|
434
|
+
backlog = chatTailBacklog(db.chatMessageFeed(), filter, limit);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error(`[thatch chat tail] feed query failed: ${err}`);
|
|
437
|
+
console.error("The database may be locked or missing; exiting.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
state = backlog.state;
|
|
441
|
+
if (backlog.elided > 0) {
|
|
442
|
+
// The elision note goes to stderr so stdout stays a clean card
|
|
443
|
+
// stream for pipes.
|
|
444
|
+
console.error(
|
|
445
|
+
`[thatch chat tail] showing last ${backlog.events.length} of ${backlog.elided + backlog.events.length} messages (--limit N, --limit all)`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
console.log(backlog.events.map(formatChatTailCard).join(`\n\n${CHAT_TAIL_SEPARATOR}\n\n`));
|
|
449
|
+
if (onceOnly || (filter.untilMs !== null && filter.untilMs <= Date.now())) break;
|
|
450
|
+
|
|
451
|
+
process.on("SIGINT", () => {
|
|
452
|
+
db.close();
|
|
453
|
+
process.exit(0);
|
|
454
|
+
});
|
|
455
|
+
for (;;) {
|
|
456
|
+
await Bun.sleep(2000);
|
|
457
|
+
try {
|
|
458
|
+
// Follow polls re-filter the live feed: matching messages
|
|
459
|
+
// surface as they arrive, non-matching ones stay invisible.
|
|
460
|
+
for (const event of chatTailDiff(state, filterChatTailRows(db.chatMessageFeed(), filter)).events) {
|
|
461
|
+
// Separate follow-mode cards from whatever came before.
|
|
462
|
+
if (firstFollowEvent && backlog.events.length > 0) console.log(`\n${CHAT_TAIL_SEPARATOR}\n`);
|
|
463
|
+
firstFollowEvent = false;
|
|
464
|
+
console.log(formatChatTailCard(event));
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
// The surface's whole job is watching a shared, mutable file;
|
|
468
|
+
// a feed failure should say so, not die with a stack trace.
|
|
469
|
+
console.error(`[thatch chat tail] feed query failed: ${err}`);
|
|
470
|
+
console.error("The database may be locked or missing; exiting.");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// The window check runs after the poll, so a message created
|
|
474
|
+
// inside the window but landed during the final sleep is still
|
|
475
|
+
// displayed; anything inserted after this poll has a created_at
|
|
476
|
+
// past the bound and is filtered by the window anyway.
|
|
477
|
+
if (filter.untilMs !== null && Date.now() >= filter.untilMs) break;
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
db.close();
|
|
481
|
+
// The script tail also closes; closing twice is harmless and the
|
|
482
|
+
// early-return paths need this one.
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
default:
|
|
488
|
+
usage();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
222
492
|
switch (command) {
|
|
223
493
|
case "--version":
|
|
224
494
|
case "-v":
|
|
@@ -327,7 +597,11 @@ switch (command) {
|
|
|
327
597
|
} catch (err) {
|
|
328
598
|
console.error(`[thatch] hygiene report failed: ${err}`);
|
|
329
599
|
}
|
|
330
|
-
|
|
600
|
+
// Chat mail check. Without a hook session ID (SessionStart does not
|
|
601
|
+
// read stdin) the line keeps the old shape: a line only when pending
|
|
602
|
+
// mail exists for registered sessions in this project.
|
|
603
|
+
const chatLine = await chatHookLine();
|
|
604
|
+
const reminder = claudeSessionStartReminder(repo, hygiene) + (chatLine ? `\n\n${chatLine}` : "");
|
|
331
605
|
if (isJson) {
|
|
332
606
|
console.log(JSON.stringify({ additional_context: reminder }));
|
|
333
607
|
} else {
|
|
@@ -383,12 +657,19 @@ switch (command) {
|
|
|
383
657
|
|
|
384
658
|
case "setup": {
|
|
385
659
|
const isGlobal = args.includes("--global");
|
|
660
|
+
// --local is the default scope; accepted explicitly so the flag the
|
|
661
|
+
// user types always matches what the command does.
|
|
662
|
+
const isLocal = args.includes("--local");
|
|
386
663
|
const isClaude = args.includes("--claude");
|
|
387
664
|
const isCursor = args.includes("--cursor");
|
|
388
665
|
|
|
666
|
+
if (isGlobal && isLocal) {
|
|
667
|
+
console.error("Choose one scope: --local or --global.");
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
389
670
|
if (!isClaude && !isCursor) {
|
|
390
671
|
console.error("Specify at least one host: --claude and/or --cursor.");
|
|
391
|
-
console.error("Usage: thatch setup --claude [--cursor] [--global]");
|
|
672
|
+
console.error("Usage: thatch setup --claude [--cursor] [--local|--global]");
|
|
392
673
|
process.exit(1);
|
|
393
674
|
}
|
|
394
675
|
|
|
@@ -400,6 +681,22 @@ switch (command) {
|
|
|
400
681
|
const thatchBin = thatchOnPath ?? resolve(process.argv[1] ?? process.execPath);
|
|
401
682
|
const dir = projectDir();
|
|
402
683
|
|
|
684
|
+
// Shared reporting: where skills went and what the pass did, so an
|
|
685
|
+
// idempotent re-run still tells the user something true.
|
|
686
|
+
const printSkills = (result: { skills: { dir: string; added: unknown[]; updated: unknown[]; removed: string[]; unchanged: string[] }; otherScopeSkills: { dir: string; count: number } | null; global: boolean }) => {
|
|
687
|
+
const { added, updated, removed, unchanged } = result.skills;
|
|
688
|
+
console.log(` Skills: ${result.skills.dir}`);
|
|
689
|
+
console.log(` ${added.length} added, ${updated.length} updated, ${removed.length} removed, ${unchanged.length} unchanged`);
|
|
690
|
+
if (result.otherScopeSkills) {
|
|
691
|
+
const { dir: otherDir, count } = result.otherScopeSkills;
|
|
692
|
+
if (result.global) {
|
|
693
|
+
console.log(` Note: ${count} thatch skills exist in ${otherDir} (project scope); --global does not update them.`);
|
|
694
|
+
} else {
|
|
695
|
+
console.log(` Note: ${count} thatch skills remain in ${otherDir} (user scope; not updated by this run). Run setup with --global to refresh them, or remove them if this repo now owns its skills.`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
403
700
|
if (isClaude) {
|
|
404
701
|
const result = setupClaudeCode(thatchBin, isGlobal, dir);
|
|
405
702
|
|
|
@@ -410,10 +707,7 @@ switch (command) {
|
|
|
410
707
|
}
|
|
411
708
|
console.log(` Instructions: ${result.claudeMd}`);
|
|
412
709
|
console.log(` Hooks: ${result.settings}`);
|
|
413
|
-
|
|
414
|
-
? result.skills[0].path.split("/").slice(0, -1).join("/")
|
|
415
|
-
: "(no skills installed)";
|
|
416
|
-
console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
|
|
710
|
+
printSkills(result);
|
|
417
711
|
console.log();
|
|
418
712
|
if (result.mcpAddCommand) {
|
|
419
713
|
console.log("To register the MCP server globally, run:");
|
|
@@ -432,10 +726,7 @@ switch (command) {
|
|
|
432
726
|
console.log(` MCP config: ${result.mcpConfig}`);
|
|
433
727
|
console.log(` Instructions: ${result.agentsMd}`);
|
|
434
728
|
console.log(` Hooks: ${result.hooks}`);
|
|
435
|
-
|
|
436
|
-
? result.skills[0].path.split("/").slice(0, -1).join("/")
|
|
437
|
-
: "(no skills installed)";
|
|
438
|
-
console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
|
|
729
|
+
printSkills(result);
|
|
439
730
|
console.log();
|
|
440
731
|
console.log("Restart Cursor for the changes to take effect.");
|
|
441
732
|
}
|
|
@@ -447,6 +738,11 @@ switch (command) {
|
|
|
447
738
|
break;
|
|
448
739
|
}
|
|
449
740
|
|
|
741
|
+
case "chat": {
|
|
742
|
+
await chatCommand(args);
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
|
|
450
746
|
case "buffer-batch": {
|
|
451
747
|
// PostToolBatch hook: append the batch's tool_calls to the session's
|
|
452
748
|
// JSONL queue. Silent on success - no stdout - so the next model call
|
|
@@ -545,6 +841,10 @@ switch (command) {
|
|
|
545
841
|
// flush-tools fires on every prompt, including conversational turns
|
|
546
842
|
// with no tool calls.
|
|
547
843
|
const versionWarning = detectSkew(dbPath) ?? readNpmCacheForUpdate(dbPath);
|
|
844
|
+
// Chat check: with a hook session ID this always prints the caller's
|
|
845
|
+
// assigned identity (and its unread count), so the model knows who it
|
|
846
|
+
// is before any chat tool call; with no mail the line says so.
|
|
847
|
+
const chatLine = await chatHookLine(sessionID);
|
|
548
848
|
|
|
549
849
|
// Tier 1: extraction nudge. Peek (don't drain) - the queue persists
|
|
550
850
|
// until the agent calls memory_remember, which triggers consumeQueue.
|
|
@@ -556,7 +856,7 @@ switch (command) {
|
|
|
556
856
|
const missed = sessionID ? getMissedCount(sessionID) : 0;
|
|
557
857
|
if (sessionID) incrementMissedCount(sessionID);
|
|
558
858
|
const output = extractionNudge(interactions.length, missed, "mcp__thatch__memory_remember", sessionID ?? "");
|
|
559
|
-
emit(output, versionWarning, isJson);
|
|
859
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
560
860
|
break;
|
|
561
861
|
}
|
|
562
862
|
|
|
@@ -590,14 +890,14 @@ switch (command) {
|
|
|
590
890
|
|
|
591
891
|
if (parts.length > 0) {
|
|
592
892
|
const output = parts.join("\n\n");
|
|
593
|
-
emit(output, versionWarning, isJson);
|
|
893
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
594
894
|
break;
|
|
595
895
|
}
|
|
596
896
|
}
|
|
597
897
|
|
|
598
898
|
// Tier 3: static write nudge.
|
|
599
899
|
const output = claudeWriteNudge();
|
|
600
|
-
emit(output, versionWarning, isJson);
|
|
900
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
601
901
|
} catch (err) {
|
|
602
902
|
console.error(`[thatch flush-tools] failed: ${err}`);
|
|
603
903
|
try {
|
package/docs/dev/README.md
CHANGED
|
@@ -40,8 +40,11 @@ OpenCode plugin path
|
|
|
40
40
|
├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
|
|
41
41
|
├── tools.ts → thin opencode tool() wrappers over tool-defs
|
|
42
42
|
├── extraction.ts → in-memory ring buffer + shared payload builders
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
├── watchers.ts → in-memory watcher registry + poller (opencode-only,
|
|
44
|
+
│ event-driven PR notifications delivered as prompts)
|
|
45
|
+
└── chat.ts → cross-session chat: shared SQLite directory + inbox
|
|
46
|
+
(ChatStore) and the per-process wake-up poller
|
|
47
|
+
(ChatPoller, opencode-only)
|
|
45
48
|
|
|
46
49
|
MCP server path
|
|
47
50
|
├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
|
|
@@ -54,7 +57,8 @@ MCP server path
|
|
|
54
57
|
CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
|
|
55
58
|
|
|
56
59
|
bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
|
|
57
|
-
prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup
|
|
60
|
+
prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup|
|
|
61
|
+
session (archaeology)|chat (list|tail)
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
## Module responsibilities
|
|
@@ -75,9 +79,11 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
75
79
|
| `config.ts` | User config file (`~/.config/thatch/config.json`, beside the DB): Zod section schemas (strict — unknown keys fail validation), load with missing/invalid fallback to defaults, field-level merge, atomic save (temp + rename). Backs the `config_get`/`config_set` tools. |
|
|
76
80
|
| `notify.ts` | Out-of-band notification dispatch: darwin (`osascript` banner + `/usr/bin/say` voice), linux (`notify-send` + `spd-say`/`espeak`), other platforms report unsupported. Injectable spawner (`CoreContext.spawner`) so tests never fire real commands. Backs the `notify_user` tool. |
|
|
77
81
|
| `session-db.ts` | Read-only access to the opencode session database (opencode.db): session timeline (`session list`), full part/message retrieval (`session get`), OpenAI chat-completions transcript builder (`session transcript`), and decoded-content search (`session search`). Backs the `thatch session` CLI subcommands and the opencode-only `session_search`/`session_get` tools. Path resolution honors `OPENCODE_DB`, then the XDG default; the db opens read-only so a thatch bug cannot corrupt host history. |
|
|
78
|
-
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only).
|
|
82
|
+
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only). Three sources: GitHub PRs and branches via `gh api` (injected `GhRunner`), and local shell commands via `bash -c` (injected `CommandRunner`) run as condition variables - only the exit code is read, never the command's output. Diffs against last-seen state, queues events (pointer data plus machine status like check conclusions and exit codes, never external content) per session, and delivers them through a plugin-supplied callback when the session is idle. The `pollSeconds` getter surfaces the poll cadence to tool output and notifications. Process-lifetime state by design - no SQLite. |
|
|
83
|
+
| `chat.ts` | Cross-session chat between opencode sessions on one machine (opencode-only). `ChatStore` holds the shared directory and inbox in SQLite so any process can read/write them; `ChatPoller` heartbeats hosted sessions and delivers wake prompts through an injected callback when the recipient is idle - only the recipient's host process delivers, so there is no cross-process double-delivery. Heartbeat staleness flags crashed sessions; a per-recipient nudge rate cap is the anti-loop hard brake. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
84
|
+
| `chat-names.ts` | The static display-name pool for the chat directory: whimsical geek-culture names in fnord's Nomenclater style, baked in so name assignment never costs a model call. The pool is the fallback base for registrations with no usable session title. |
|
|
79
85
|
| `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
|
|
80
|
-
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
|
|
86
|
+
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), watcher notification (`watcherNotificationNudge`), version warning (`versionWarningNudge`), chat wake notification (`chatNotificationNudge`), chat transcript echo text and echo recognizer (`chatEchoText` / `isChatEchoParts`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
|
|
81
87
|
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (fact-extractor, dedup-classifier, project-primer, the review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr, memory-verify, knowledge-export — work on all three hosts) and `OPENCODE_ONLY_SKILLS` (code-review coordinator — requires sub-agent support, not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
|
|
82
88
|
| `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
|
|
83
89
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
@@ -94,10 +100,10 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
94
100
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
95
101
|
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
96
102
|
| `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
|
|
97
|
-
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
98
|
-
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. The buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
|
|
99
|
-
| `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status`: records the session's live status for the watcher delivery
|
|
100
|
-
| `dispose` | Stops the version checker and the
|
|
103
|
+
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. For `thatch_chat_*` tool calls, builds the transcript echo (`chatEchoText`) and posts it as a visible non-synthetic `noReply` part. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
104
|
+
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. The buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Chat transcript echoes (non-synthetic `noReply` parts whose text is entirely `[chat]`-prefixed bubbles) are skipped before any tier — no model turn reads them, so nudging them wastes an embedding. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
|
|
105
|
+
| `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status`: records the session's live status for the watcher and chat delivery gates; on idle, if the session is a child, drains its snapshot, fires a toast with `childMetrics`, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls `triggerExtraction` to create a direct-extraction child, then flushes pending watcher events and pending chat mail (the session just became deliverable). `session.error`: requeues the parent's buffer (child died without draining). `session.deleted`: cleans up `childToParent`, `parentSnapshots`, `childMetrics`, and `extracting`, cancels the session's watchers, and unregisters it from the chat directory (the crash path — a dead process fires no event — is covered by heartbeat staleness). `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
|
|
106
|
+
| `dispose` | Stops the version checker, the watcher poller, and the chat poller, closes the DB. |
|
|
101
107
|
|
|
102
108
|
Hook failures are logged with a `[thatch]` prefix — never swallowed silently.
|
|
103
109
|
Two of these hooks were dead for weeks because failures were invisible.
|
|
@@ -119,10 +125,12 @@ Two of these hooks were dead for weeks because failures were invisible.
|
|
|
119
125
|
5. **Store creation is implicit.** First `remember` to a new store creates it.
|
|
120
126
|
6. **Default recall scope is repo + global.** The tool layer hardcodes this.
|
|
121
127
|
7. **Skills are plugin-owned files.** Installed to
|
|
122
|
-
`$XDG_CONFIG_HOME/opencode/skills` (opencode),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
`$XDG_CONFIG_HOME/opencode/skills` (opencode); for MCP hosts,
|
|
129
|
+
scope-dependent — the repo's `.claude/skills/` or `.cursor/skills/`
|
|
130
|
+
for project-local `thatch setup`, `~/.claude/skills/` or
|
|
131
|
+
`~/.cursor/skills/` for `--global`. Drifted content is overwritten
|
|
132
|
+
on plugin init or re-running `thatch setup`. Skills are split into
|
|
133
|
+
`SHARED_SKILLS` (work on all
|
|
126
134
|
three hosts) and `OPENCODE_ONLY_SKILLS` (require sub-agent support,
|
|
127
135
|
not installed for Claude Code or Cursor). The opencode plugin
|
|
128
136
|
installs both arrays; `thatch setup --claude` and `--cursor` install
|
|
@@ -328,12 +336,18 @@ behavior cycle (agent-driven, self-graded)
|
|
|
328
336
|
`behavior_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
|
|
329
337
|
`behaviors(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
|
|
330
338
|
`behavior_edges(matcher_id, behavior_id, weight REAL, PK(matcher_id, behavior_id), FK CASCADE)`,
|
|
331
|
-
`behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)
|
|
339
|
+
`behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)`,
|
|
340
|
+
`chat_sessions(session_id PK, name UNIQUE COLLATE NOCASE, topic, project, host_kind, registered_at, last_seen, auto)`,
|
|
341
|
+
`chat_messages(id PK AUTOINCREMENT, from_session, to_session, body, created_at, delivered_at, read_at, via_broadcast)`,
|
|
342
|
+
`chat_name_counters(base PK, next)` - per-base counters for assigned chat names; only ever increments.
|
|
343
|
+
`chat_leave_tombstones(session_id PK, left_at)` - explicit leaves; suppresses auto-registration until chat_register rejoins.
|
|
332
344
|
- `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
|
|
333
345
|
databases by an idempotent column migration at init (`PRAGMA table_info` +
|
|
334
346
|
`ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =
|
|
335
347
|
live, 1 = archived); search, dedup, and staleness queries all exclude
|
|
336
|
-
archived entries by default.
|
|
348
|
+
archived entries by default. A second init migration rebuilds a
|
|
349
|
+
pre-NOCASE `chat_sessions` table to enforce case-insensitive name
|
|
350
|
+
uniqueness (see [features/database.md](features/database.md)).
|
|
337
351
|
- Embeddings are raw Float32Array bytes. Serialization honors
|
|
338
352
|
`byteOffset`/`byteLength` — transformers.js can return views into larger
|
|
339
353
|
tensor buffers, and serializing the whole backing buffer corrupts vectors.
|
|
@@ -27,6 +27,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
27
27
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
28
28
|
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking) |
|
|
29
29
|
| [watchers.md](watchers.md) | Event-driven notifications (in-memory registry, background poller, PR watching) |
|
|
30
|
+
| [cross-session-chat.md](cross-session-chat.md) | Cross-session chat (shared SQLite directory and inbox, per-process wake-up delivery) |
|
|
30
31
|
| [notifications.md](notifications.md) | Out-of-band notification (banner + voice) and the user config file |
|
|
31
32
|
| [setup.md](setup.md) | Setup system (markers, checkSetup, auto-refresh, binary resolution) |
|
|
32
33
|
| [cli.md](cli.md) | CLI (subcommands, environment variables) |
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# CLI (bin/thatch)
|
|
2
2
|
|
|
3
|
-
The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, and the
|
|
3
|
+
The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, the setup installer, session archaeology over the opencode database, and a read-only window on the cross-session chat directory.
|
|
4
4
|
|
|
5
5
|
## Subcommands
|
|
6
6
|
|
|
7
|
-
15 subcommands:
|
|
8
|
-
|
|
9
7
|
| Command | Args | Flags | Stdin | Purpose |
|
|
10
8
|
|---------|------|-------|-------|---------|
|
|
11
9
|
| `stores` | none | none | none | List all store names |
|
|
@@ -22,6 +20,9 @@ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, M
|
|
|
22
20
|
| `flush-tools [--json]` | none | `--json` | JSON | Peek queue + extraction/recall/prediction/behavior/write nudge |
|
|
23
21
|
| `flush-predictions [--json]` | none | `--json` | JSON | Standalone prediction-only nudge |
|
|
24
22
|
| `setup --claude [--cursor] [--global]` | none | `--claude`, `--cursor`, `--global` | none | Install config + instructions + hooks + skills |
|
|
23
|
+
| `session list/get/transcript/search` | per subcommand | `-s/--session`, `--id`, `--after`, `--before`, `--regex`, `--limit` | none | Read-only archaeology on the opencode session database (JSONL output) |
|
|
24
|
+
| `chat list` | none | none | none | Registered chat sessions: name, human-readable age, project, topic |
|
|
25
|
+
| `chat tail` | none | `--once`, `--limit N\|all`, `--match RE` (repeatable), `--from NAME`, `--to NAME`, `--since DT`, `--until DT` | none | Follow cross-session chat: sent and read events. Backlog defaults to the last 20 messages; filters apply to follow mode too. |
|
|
25
26
|
| (unknown) | none | none | none | Print usage, exit 1 |
|
|
26
27
|
|
|
27
28
|
## Global behavior
|
|
@@ -41,6 +42,32 @@ Runs the thatch-project-primer skill via an external CLI. Searches `PATH` in ord
|
|
|
41
42
|
|
|
42
43
|
Inherits stdio, exits with the child's exit code. Errors and exits 1 if none found.
|
|
43
44
|
|
|
45
|
+
## chat
|
|
46
|
+
|
|
47
|
+
Read-only window on the cross-session chat directory ([cross-session-chat.md](cross-session-chat.md)):
|
|
48
|
+
|
|
49
|
+
- `chat list` renders the roster: display name, human-readable age since the
|
|
50
|
+
last heartbeat, project, and topic when set.
|
|
51
|
+
- `chat tail` follows the message stream as styled cards: label chips,
|
|
52
|
+
colored names, dimmed timestamps, topic annotations after names, and a
|
|
53
|
+
drawn rule between cards. The card renderer is `formatChatTailCard()`
|
|
54
|
+
and the event shaping is `chatTailDiff()` in `src/chat.ts`, unit-tested
|
|
55
|
+
there; the single-line `formatChatTailEvent()` remains for tests and
|
|
56
|
+
compact contexts. `--once` prints one snapshot and exits, because a
|
|
57
|
+
snapshot has no previous state to diff read events against.
|
|
58
|
+
- The backlog renders only the last `CHAT_TAIL_DEFAULT_LIMIT` (20) messages;
|
|
59
|
+
`chatTailBacklog()` in `src/chat.ts` filters the feed, seeds the diff
|
|
60
|
+
state from every feed row (so the limit hides cards, not history, and a
|
|
61
|
+
mid-follow rename or unregister cannot resurface old rows as sent
|
|
62
|
+
events), and reports the elided count for
|
|
63
|
+
the CLI's stderr note. `filterChatTailRows()` ANDs body regexes
|
|
64
|
+
(`--match`, repeatable), rendered-name substrings (`--from`/`--to`,
|
|
65
|
+
case-insensitive), and a half-open `--since`/`--until` window on
|
|
66
|
+
`created_at`; the same filter applies to follow polls, so non-matching
|
|
67
|
+
messages stay invisible (their read events too). A `--until` in the past
|
|
68
|
+
exits after the backlog; one in the future follows until the window
|
|
69
|
+
closes. `parseChatTimeBound()` accepts `YYYY-MM-DD [HH:MM]` local time.
|
|
70
|
+
|
|
44
71
|
## Environment variables
|
|
45
72
|
|
|
46
73
|
| Variable | Purpose | Default |
|
|
@@ -59,7 +86,7 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
59
86
|
|
|
60
87
|
## Source files
|
|
61
88
|
|
|
62
|
-
- `bin/thatch` — the CLI (Bun script
|
|
89
|
+
- `bin/thatch` — the CLI (Bun script)
|
|
63
90
|
- `bin/release` — release helper (bash, separate from the main CLI)
|
|
64
91
|
|
|
65
92
|
## Interactions with other features
|
|
@@ -70,6 +97,8 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
70
97
|
- Extraction ([extraction.md](extraction.md)): `buffer-batch`, `buffer-tool`, `flush-tools` subcommands
|
|
71
98
|
- Hygiene ([hygiene.md](hygiene.md)): `hygiene`, `reminder` subcommands
|
|
72
99
|
- Multi-host ([multi-host.md](multi-host.md)): `mcp` subcommand starts the MCP server for Claude Code and Cursor
|
|
100
|
+
- Session archaeology: the `session` subcommand group (see the table above) reads the opencode session database via `src/session-db.ts`
|
|
101
|
+
- Cross-session chat ([cross-session-chat.md](cross-session-chat.md)): the `chat` subcommand group reads the shared chat directory and inbox
|
|
73
102
|
|
|
74
103
|
## Key invariants
|
|
75
104
|
|