@jeffober/thatch 0.1.40 → 0.1.41
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 +10 -3
- 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/thatch +160 -16
- package/docs/dev/README.md +26 -14
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/cli.md +22 -4
- package/docs/dev/features/cross-session-chat.md +256 -0
- package/docs/dev/features/database.md +52 -4
- package/docs/dev/features/multi-host.md +17 -9
- package/docs/dev/features/setup.md +12 -5
- 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 +23 -5
- package/docs/user/cli.md +30 -0
- package/docs/user/code-review.md +1 -1
- package/docs/user/cross-session-chat.md +102 -0
- package/docs/user/setup.md +16 -2
- package/docs/user/skills.md +7 -6
- package/package.json +1 -1
- package/src/chat-names.ts +121 -0
- package/src/chat.ts +879 -0
- package/src/db.ts +179 -0
- package/src/index.ts +113 -5
- package/src/prompts.ts +133 -8
- package/src/setup.ts +83 -29
- package/src/skills.ts +47 -17
- package/src/tool-defs.ts +346 -0
package/src/setup.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { claudeInstructions, cursorInstructions } from "./prompts";
|
|
4
|
-
import { installSkills, type
|
|
4
|
+
import { installSkills, type InstallReport } from "./skills";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Path resolution
|
|
@@ -14,7 +14,7 @@ interface SetupPaths {
|
|
|
14
14
|
claudeMdPath: string;
|
|
15
15
|
/** .claude/settings.json or $CLAUDE_CONFIG_DIR/settings.json for hooks. */
|
|
16
16
|
settingsPath: string;
|
|
17
|
-
/** Skills directory
|
|
17
|
+
/** Skills directory: project .claude/skills/ (local) or config-dir skills/ (global). */
|
|
18
18
|
skillsDir: string;
|
|
19
19
|
/** Whether this is a global install. */
|
|
20
20
|
global: boolean;
|
|
@@ -24,9 +24,7 @@ interface SetupPaths {
|
|
|
24
24
|
* Resolve the Claude config directory. `CLAUDE_CONFIG_DIR` overrides the
|
|
25
25
|
* default `~/.claude` location - used by people running multiple accounts
|
|
26
26
|
* side by side (per Claude Code env-vars docs). Settings, CLAUDE.md, and
|
|
27
|
-
* skills are all stored under this path in global scope.
|
|
28
|
-
* installs only use it for skills (which are always user-scoped); the
|
|
29
|
-
* project's own .claude/settings.json and CLAUDE.md stay in the repo.
|
|
27
|
+
* skills are all stored under this path in global scope.
|
|
30
28
|
*/
|
|
31
29
|
function claudeConfigDir(homeDir: string): string {
|
|
32
30
|
return process.env.CLAUDE_CONFIG_DIR ?? join(homeDir, ".claude");
|
|
@@ -34,7 +32,6 @@ function claudeConfigDir(homeDir: string): string {
|
|
|
34
32
|
|
|
35
33
|
function resolvePaths(global: boolean, projectDir: string, homeDir: string): SetupPaths {
|
|
36
34
|
const configDir = claudeConfigDir(homeDir);
|
|
37
|
-
const skillsDir = join(configDir, "skills");
|
|
38
35
|
|
|
39
36
|
if (global) {
|
|
40
37
|
return {
|
|
@@ -45,7 +42,9 @@ function resolvePaths(global: boolean, projectDir: string, homeDir: string): Set
|
|
|
45
42
|
// here also fixes a pre-existing bug where we wrote to `~/CLAUDE.md`.
|
|
46
43
|
claudeMdPath: join(configDir, "CLAUDE.md"),
|
|
47
44
|
settingsPath: join(configDir, "settings.json"),
|
|
48
|
-
|
|
45
|
+
// Global installs put skills where every project can read them:
|
|
46
|
+
// the Claude config dir (Claude Code auto-discovers it).
|
|
47
|
+
skillsDir: join(configDir, "skills"),
|
|
49
48
|
global: true,
|
|
50
49
|
};
|
|
51
50
|
}
|
|
@@ -54,9 +53,10 @@ function resolvePaths(global: boolean, projectDir: string, homeDir: string): Set
|
|
|
54
53
|
mcpConfigPath: join(projectDir, ".mcp.json"),
|
|
55
54
|
claudeMdPath: join(projectDir, "CLAUDE.md"),
|
|
56
55
|
settingsPath: join(projectDir, ".claude", "settings.json"),
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
56
|
+
// Project-local installs keep skills in the repo so they version with
|
|
57
|
+
// the project and every contributor gets them. Claude Code discovers
|
|
58
|
+
// project skills at .claude/skills/ alongside its own project config.
|
|
59
|
+
skillsDir: join(projectDir, ".claude", "skills"),
|
|
60
60
|
global: false,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -208,19 +208,51 @@ function replaceThatchHooks(
|
|
|
208
208
|
return filtered;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
// Skills - install to $CLAUDE_CONFIG_DIR/skills/ (or ~/.claude/skills/ by default)
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
|
|
215
211
|
// ---------------------------------------------------------------------------
|
|
216
212
|
// Main setup entry point
|
|
217
213
|
// ---------------------------------------------------------------------------
|
|
218
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Thatch skills found in the OTHER scope's directory. A local run checks the
|
|
217
|
+
* user config dir; a global run checks the project. Setup never touches the
|
|
218
|
+
* other scope, so these copies drift on their own - surfacing them lets the
|
|
219
|
+
* CLI warn about leftovers (e.g. user-scope skills from before a repo moved
|
|
220
|
+
* to project-local installs).
|
|
221
|
+
*/
|
|
222
|
+
export interface OtherScopeSkills {
|
|
223
|
+
dir: string;
|
|
224
|
+
count: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Count thatch-* skill directories (or symlinks) under a skills dir without
|
|
229
|
+
* touching anything. Returns 0 when the dir doesn't exist.
|
|
230
|
+
*/
|
|
231
|
+
function countThatchSkills(dir: string): number {
|
|
232
|
+
let entries;
|
|
233
|
+
try {
|
|
234
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
235
|
+
} catch {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
return entries.filter(
|
|
239
|
+
(e) => (e.isDirectory() || e.isSymbolicLink()) && e.name.startsWith("thatch-"),
|
|
240
|
+
).length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function otherScopeInfo(dir: string): OtherScopeSkills | null {
|
|
244
|
+
const count = countThatchSkills(dir);
|
|
245
|
+
return count > 0 ? { dir, count } : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
export interface SetupResult {
|
|
220
249
|
mcpConfig: string | null;
|
|
221
250
|
claudeMd: string;
|
|
222
251
|
settings: string;
|
|
223
|
-
|
|
252
|
+
/** Skill install report for the directory this run wrote to. */
|
|
253
|
+
skills: InstallReport;
|
|
254
|
+
/** Thatch skills in the opposite scope's dir, if any; never written by this run. */
|
|
255
|
+
otherScopeSkills: OtherScopeSkills | null;
|
|
224
256
|
global: boolean;
|
|
225
257
|
/** For global installs, the `claude mcp add` command to run. */
|
|
226
258
|
mcpAddCommand: string | null;
|
|
@@ -232,9 +264,9 @@ export interface SetupResult {
|
|
|
232
264
|
* and installs skill files. All operations are idempotent - re-running setup
|
|
233
265
|
* updates content that has drifted without clobbering unrelated configuration.
|
|
234
266
|
*
|
|
235
|
-
*
|
|
236
|
-
* skills
|
|
237
|
-
*
|
|
267
|
+
* Skills follow the install scope: project-local runs write them to the
|
|
268
|
+
* repo's .claude/skills/, global runs to $CLAUDE_CONFIG_DIR/skills/. Honors
|
|
269
|
+
* `CLAUDE_CONFIG_DIR` for all user-scoped paths (settings, CLAUDE.md, skills).
|
|
238
270
|
*
|
|
239
271
|
* @param thatchBin Absolute path to the thatch binary
|
|
240
272
|
* @param global Whether to install globally or project-locally
|
|
@@ -259,11 +291,19 @@ export function setupClaudeCode(
|
|
|
259
291
|
writeHooks(paths.settingsPath, thatchBin);
|
|
260
292
|
const skills = installSkills(paths.skillsDir);
|
|
261
293
|
|
|
294
|
+
// The scope this run did NOT write to. Local runs leave user-scope copies
|
|
295
|
+
// behind (pre-existing global installs); global runs leave project-local
|
|
296
|
+
// copies in the repo. Either way the user should hear about them.
|
|
297
|
+
const otherDir = global
|
|
298
|
+
? join(projectDir, ".claude", "skills")
|
|
299
|
+
: join(claudeConfigDir(home), "skills");
|
|
300
|
+
|
|
262
301
|
return {
|
|
263
302
|
mcpConfig: paths.mcpConfigPath,
|
|
264
303
|
claudeMd: paths.claudeMdPath,
|
|
265
304
|
settings: paths.settingsPath,
|
|
266
305
|
skills,
|
|
306
|
+
otherScopeSkills: otherScopeInfo(otherDir),
|
|
267
307
|
global,
|
|
268
308
|
mcpAddCommand: global
|
|
269
309
|
? `claude mcp add --scope user thatch -- ${thatchBin} mcp`
|
|
@@ -295,7 +335,6 @@ interface CursorSetupPaths {
|
|
|
295
335
|
|
|
296
336
|
function resolveCursorPaths(global: boolean, projectDir: string, homeDir: string): CursorSetupPaths {
|
|
297
337
|
const configDir = cursorConfigDir(homeDir);
|
|
298
|
-
const skillsDir = join(configDir, "skills");
|
|
299
338
|
|
|
300
339
|
if (global) {
|
|
301
340
|
return {
|
|
@@ -304,7 +343,8 @@ function resolveCursorPaths(global: boolean, projectDir: string, homeDir: string
|
|
|
304
343
|
mcpConfigPath: join(configDir, "mcp.json"),
|
|
305
344
|
agentsMdPath: join(configDir, "AGENTS.md"),
|
|
306
345
|
hooksPath: join(configDir, "hooks.json"),
|
|
307
|
-
|
|
346
|
+
// Global installs put skills where every project can read them.
|
|
347
|
+
skillsDir: join(configDir, "skills"),
|
|
308
348
|
global: true,
|
|
309
349
|
};
|
|
310
350
|
}
|
|
@@ -313,7 +353,9 @@ function resolveCursorPaths(global: boolean, projectDir: string, homeDir: string
|
|
|
313
353
|
mcpConfigPath: join(projectDir, ".cursor", "mcp.json"),
|
|
314
354
|
agentsMdPath: join(projectDir, "AGENTS.md"),
|
|
315
355
|
hooksPath: join(projectDir, ".cursor", "hooks.json"),
|
|
316
|
-
|
|
356
|
+
// Project-local installs keep skills in the repo; Cursor auto-discovers
|
|
357
|
+
// project skills at .cursor/skills/.
|
|
358
|
+
skillsDir: join(projectDir, ".cursor", "skills"),
|
|
317
359
|
global: false,
|
|
318
360
|
};
|
|
319
361
|
}
|
|
@@ -374,7 +416,10 @@ export interface CursorSetupResult {
|
|
|
374
416
|
mcpConfig: string;
|
|
375
417
|
agentsMd: string;
|
|
376
418
|
hooks: string;
|
|
377
|
-
|
|
419
|
+
/** Skill install report for the directory this run wrote to. */
|
|
420
|
+
skills: InstallReport;
|
|
421
|
+
/** Thatch skills in the opposite scope's dir, if any; never written by this run. */
|
|
422
|
+
otherScopeSkills: OtherScopeSkills | null;
|
|
378
423
|
global: boolean;
|
|
379
424
|
}
|
|
380
425
|
|
|
@@ -385,8 +430,10 @@ export interface CursorSetupResult {
|
|
|
385
430
|
* skill files. All operations are idempotent.
|
|
386
431
|
*
|
|
387
432
|
* Cursor's global MCP config is a simple file write (unlike Claude Code which
|
|
388
|
-
* needs `claude mcp add --scope user`). Skills install
|
|
389
|
-
*
|
|
433
|
+
* needs `claude mcp add --scope user`). Skills follow the install scope:
|
|
434
|
+
* project-local runs write them to the repo's .cursor/skills/, global runs to
|
|
435
|
+
* the Cursor config dir's skills/ (Cursor also reads .claude/skills/ dirs
|
|
436
|
+
* for compatibility).
|
|
390
437
|
*
|
|
391
438
|
* @param thatchBin Absolute path to the thatch binary
|
|
392
439
|
* @param global Whether to install globally or project-locally
|
|
@@ -408,13 +455,20 @@ export function setupCursor(
|
|
|
408
455
|
writeCursorHooks(paths.hooksPath, thatchBin);
|
|
409
456
|
const skills = installSkills(paths.skillsDir);
|
|
410
457
|
|
|
458
|
+
// Same other-scope logic as the Claude path: surface skills this run did
|
|
459
|
+
// not touch so the CLI can warn about leftovers.
|
|
460
|
+
const otherDir = global
|
|
461
|
+
? join(projectDir, ".cursor", "skills")
|
|
462
|
+
: join(cursorConfigDir(home), "skills");
|
|
463
|
+
|
|
411
464
|
return {
|
|
412
465
|
mcpConfig: paths.mcpConfigPath,
|
|
413
466
|
agentsMd: paths.agentsMdPath,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
467
|
+
hooks: paths.hooksPath,
|
|
468
|
+
skills,
|
|
469
|
+
otherScopeSkills: otherScopeInfo(otherDir),
|
|
470
|
+
global,
|
|
471
|
+
};
|
|
418
472
|
}
|
|
419
473
|
|
|
420
474
|
// ---------------------------------------------------------------------------
|
package/src/skills.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
export interface
|
|
4
|
+
export interface SkillDef {
|
|
5
5
|
name: string;
|
|
6
|
-
path: string;
|
|
7
6
|
content: string;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
/** A skill file that exists on disk after an install pass. */
|
|
10
|
+
export interface InstalledSkill {
|
|
11
11
|
name: string;
|
|
12
|
-
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Outcome of one installSkills pass over a directory. Every skill in the
|
|
17
|
+
* install set lands in exactly one of added/updated/unchanged; removed lists
|
|
18
|
+
* stale thatch-* directories pruned before the write pass. The CLI reports
|
|
19
|
+
* these counts so a re-run tells the user what actually happened instead of
|
|
20
|
+
* printing nothing when everything was already current.
|
|
21
|
+
*/
|
|
22
|
+
export interface InstallReport {
|
|
23
|
+
/** Directory the skills were installed to. */
|
|
24
|
+
dir: string;
|
|
25
|
+
/** Skills written for the first time. */
|
|
26
|
+
added: InstalledSkill[];
|
|
27
|
+
/** Skills whose on-disk content differed and were overwritten. */
|
|
28
|
+
updated: InstalledSkill[];
|
|
29
|
+
/** Skills already current; nothing was written. */
|
|
30
|
+
unchanged: string[];
|
|
31
|
+
/** Stale thatch-* skill directories deleted (renamed or retired skills). */
|
|
32
|
+
removed: string[];
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
function loadSkillFile(name: string): string {
|
|
@@ -41,7 +61,7 @@ function loadSharedSkills(): SkillDef[] {
|
|
|
41
61
|
"thatch-review-breadcrumbs",
|
|
42
62
|
"thatch-review-mark-and-sweep",
|
|
43
63
|
"thatch-review-highlights",
|
|
44
|
-
"thatch-review-
|
|
64
|
+
"thatch-review-technique",
|
|
45
65
|
"thatch-review-synthesizer",
|
|
46
66
|
"thatch-review-context",
|
|
47
67
|
"thatch-code-archaeology",
|
|
@@ -73,10 +93,6 @@ export const OPENCODE_ONLY_SKILLS: SkillDef[] = loadOpencodeOnlySkills();
|
|
|
73
93
|
// Stale skill cleanup - runs before install on every skillsDir
|
|
74
94
|
// ---------------------------------------------------------------------------
|
|
75
95
|
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Stale skill cleanup - runs before install on every skillsDir
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
96
|
// Skills renamed from non-prefixed to thatch-prefixed in v0.1.27
|
|
81
97
|
// (pr-description, ticket-description, split-overlarge-pr) were cleaned up
|
|
82
98
|
// during install through a version-gated window that closed with v0.1.35.
|
|
@@ -99,14 +115,18 @@ export const OPENCODE_ONLY_SKILLS: SkillDef[] = loadOpencodeOnlySkills();
|
|
|
99
115
|
* (pr-description -> thatch-pr-description and siblings) removed old
|
|
100
116
|
* non-prefixed dirs through a version window that closed with v0.1.35; see
|
|
101
117
|
* the block comment above.
|
|
118
|
+
*
|
|
119
|
+
* Returns the names of the directories it removed so the install report can
|
|
120
|
+
* surface them.
|
|
102
121
|
*/
|
|
103
|
-
function cleanupStaleSkills(skillsDir: string, currentNames: string[]):
|
|
122
|
+
function cleanupStaleSkills(skillsDir: string, currentNames: string[]): string[] {
|
|
104
123
|
const currentSet = new Set(currentNames);
|
|
124
|
+
const removed: string[] = [];
|
|
105
125
|
let entries;
|
|
106
126
|
try {
|
|
107
127
|
entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
108
128
|
} catch {
|
|
109
|
-
return; // dir doesn't exist or isn't readable - nothing to clean
|
|
129
|
+
return []; // dir doesn't exist or isn't readable - nothing to clean
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
for (const entry of entries) {
|
|
@@ -127,6 +147,7 @@ function cleanupStaleSkills(skillsDir: string, currentNames: string[]): void {
|
|
|
127
147
|
} else {
|
|
128
148
|
rmSync(fullPath, { recursive: true, force: true });
|
|
129
149
|
}
|
|
150
|
+
removed.push(name);
|
|
130
151
|
} catch {
|
|
131
152
|
// best-effort - don't block install on cleanup failure
|
|
132
153
|
}
|
|
@@ -137,16 +158,20 @@ function cleanupStaleSkills(skillsDir: string, currentNames: string[]): void {
|
|
|
137
158
|
remove();
|
|
138
159
|
}
|
|
139
160
|
}
|
|
161
|
+
|
|
162
|
+
return removed;
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
export function installSkills(
|
|
143
166
|
skillsDir: string,
|
|
144
167
|
skills: SkillDef[] = SHARED_SKILLS,
|
|
145
|
-
):
|
|
168
|
+
): InstallReport {
|
|
146
169
|
mkdirSync(skillsDir, { recursive: true });
|
|
147
|
-
cleanupStaleSkills(skillsDir, skills.map((s) => s.name));
|
|
170
|
+
const removed = cleanupStaleSkills(skillsDir, skills.map((s) => s.name));
|
|
148
171
|
|
|
149
|
-
const
|
|
172
|
+
const added: InstalledSkill[] = [];
|
|
173
|
+
const updated: InstalledSkill[] = [];
|
|
174
|
+
const unchanged: string[] = [];
|
|
150
175
|
|
|
151
176
|
for (const skill of skills) {
|
|
152
177
|
const dir = join(skillsDir, skill.name);
|
|
@@ -159,12 +184,17 @@ export function installSkills(
|
|
|
159
184
|
// missing file - first install
|
|
160
185
|
}
|
|
161
186
|
|
|
162
|
-
if (current
|
|
187
|
+
if (current === null) {
|
|
163
188
|
mkdirSync(dir, { recursive: true });
|
|
164
189
|
writeFileSync(file, skill.content);
|
|
165
|
-
|
|
190
|
+
added.push({ name: skill.name, path: file });
|
|
191
|
+
} else if (current !== skill.content) {
|
|
192
|
+
writeFileSync(file, skill.content);
|
|
193
|
+
updated.push({ name: skill.name, path: file });
|
|
194
|
+
} else {
|
|
195
|
+
unchanged.push(skill.name);
|
|
166
196
|
}
|
|
167
197
|
}
|
|
168
198
|
|
|
169
|
-
return
|
|
199
|
+
return { dir: skillsDir, added, updated, unchanged, removed };
|
|
170
200
|
}
|