@antonior/claude-code-setup 1.0.1 → 1.1.1
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 +53 -20
- package/bin/install.js +87 -22
- package/files/settings.json +15 -1
- package/files/skills/caveman/SKILL.md +62 -0
- package/files/transcript-search/rag_lite.py +335 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@antonior/claude-code-setup)
|
|
4
4
|
[](./LICENSE)
|
|
5
|
-
[](#platforms)
|
|
6
|
+
[](#platforms)
|
|
7
|
+
[](#platforms)
|
|
8
|
+
[](#optimised-for-low-token-usage)
|
|
6
9
|
|
|
7
|
-
One-command installer for my [Claude Code](https://claude.com/claude-code) configuration — hooks, slash commands, skills, statusline, and global rules. Install once, get the exact setup I use every day.
|
|
10
|
+
One-command installer for my [Claude Code](https://claude.com/claude-code) configuration — hooks, slash commands, skills, statusline, and global rules. Install once, get the exact setup I use every day. **Optimised for low token usage.**
|
|
11
|
+
|
|
12
|
+
## Why this setup?
|
|
13
|
+
|
|
14
|
+
Stock Claude Code is powerful but neutral. This config turns it into a careful, cost-aware engineer with guardrails — the difference between an assistant that *sounds* confident and one that *proves* its work.
|
|
15
|
+
|
|
16
|
+
- **🛡️ Trustworthy by default.** Built-in rules forbid the things that quietly burn you: never fakes or weakens tests, never suppresses errors with `@ts-ignore` / `eslint-disable` / swallowed `catch`, never claims "done" without verifying. Failures get surfaced, not hidden.
|
|
17
|
+
- **🔒 Catches mistakes before they ship.** `scan-secrets` blocks any commit containing API keys or a real `.env`. `stop-verify` runs lint + typecheck on the files you changed at the end of every turn and won't let "done" slide if they fail. `check-dep` forces a real look at any new dependency (size, maintenance, lighter alternatives) before it's added.
|
|
18
|
+
- **🎯 Stays in scope.** Only changes what you asked for — no drive-by refactors, no unrequested "improvements", no comments bolted onto code it didn't touch.
|
|
19
|
+
- **🧠 Remembers across sessions.** The `transcript-search` MCP indexes your *own* past conversations locally, so Claude recalls prior decisions ("like we did last time") instead of saying "I don't remember" — and instead of you re-explaining context.
|
|
20
|
+
- **💸 Cheap on tokens.** Caveman mode keeps full technical accuracy while cutting chatter ~75%; video defaults to audio-only transcription; verification hooks touch only changed files, not the whole repo. See [Optimised for low token usage](#optimised-for-low-token-usage).
|
|
21
|
+
- **🎬 Understands video.** The `claude-video-vision` MCP lets Claude watch and reason about video, not just text.
|
|
22
|
+
- **📊 Knows where you stand.** A rich statusline shows git branch + dirty state, model and effort level, context-window %, and your 5-hour / 7-day rate-limit usage at a glance.
|
|
23
|
+
- **⚡ Zero-friction, safe install.** One command installs everything, auto-installs its dependencies, registers the MCP servers, and **smart-merges** into any existing config (it never clobbers your `settings.json` or `CLAUDE.md`). Idempotent — re-run any time to pull updates.
|
|
8
24
|
|
|
9
25
|
## Install
|
|
10
26
|
|
|
@@ -14,17 +30,20 @@ npx @antonior/claude-code-setup
|
|
|
14
30
|
|
|
15
31
|
That's it. The installer is idempotent — safe to re-run to pull updates.
|
|
16
32
|
|
|
17
|
-
##
|
|
33
|
+
## Platforms
|
|
34
|
+
|
|
35
|
+
Works on **macOS, Linux, and Windows**. The config, hooks, slash commands, skills, MCP servers, and global rules run across all three. A couple of cosmetic niceties (notification sound, the statusline clock) are tuned for macOS and simply stay quiet elsewhere — nothing blocks the install or the core behaviour.
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
## Dependencies (auto-installed via Homebrew)
|
|
20
38
|
|
|
21
|
-
|
|
39
|
+
The installer installs these for you if missing:
|
|
22
40
|
|
|
23
|
-
| Tool |
|
|
24
|
-
|
|
25
|
-
| `jq` |
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
41
|
+
| Tool | For | Notes |
|
|
42
|
+
|------|-----|-------|
|
|
43
|
+
| `jq` | hooks (JSON parsing) | **Required** — install aborts without it. |
|
|
44
|
+
| `python3` | `transcript-search` MCP + video-FPS hook | Best-effort — feature stays dormant if it can't install. |
|
|
45
|
+
| `ffmpeg` | `claude-video-vision` MCP | Best-effort. |
|
|
46
|
+
| `git`, `eslint`, `tsc` | commit-scan / lint / typecheck hooks | Optional — hooks no-op cleanly when absent. |
|
|
28
47
|
|
|
29
48
|
## What it installs (into `~/.claude/`)
|
|
30
49
|
|
|
@@ -38,24 +57,38 @@ That's it. The installer is idempotent — safe to re-run to pull updates.
|
|
|
38
57
|
- `stop-verify.sh` — lint + typecheck changed files when a turn ends
|
|
39
58
|
- `notify-sound.sh` — sound on notification/stop
|
|
40
59
|
- **`commands/`** — `/check-dep`, `/debug`, `/scan-secrets` slash commands
|
|
41
|
-
- **`skills/`** — `/mute`, `/unmute`
|
|
60
|
+
- **`skills/`** — `/mute`, `/unmute`, and `/caveman` (with intensity levels)
|
|
42
61
|
- **`statusline.sh`** — git, model, context %, rate-limit statusline
|
|
62
|
+
- **`transcript-search/rag_lite.py`** — the transcript-search MCP engine
|
|
63
|
+
- **MCP servers** (registered via `claude mcp add-json`, user scope) — see below
|
|
64
|
+
|
|
65
|
+
## Bundled MCP servers
|
|
66
|
+
|
|
67
|
+
Both are installed and auto-registered for you:
|
|
68
|
+
|
|
69
|
+
- **`transcript-search`** — full-text search over your *own* past Claude Code transcripts ("we talked about…", "like last time"). Pure-Python, stdlib only. The index is built **locally on your machine** from `~/.claude/projects/` on first run — nothing about your conversations is ever shipped in this package.
|
|
70
|
+
- **`claude-video-vision`** — lets Claude watch/analyse videos (frames via `ffmpeg`). Published as [`claude-video-vision`](https://www.npmjs.com/package/claude-video-vision) on npm; run via `npx`.
|
|
71
|
+
- **Tip:** when it asks how many FPS, answer **0** to use audio transcription only (no frame extraction) — much cheaper on tokens. Bump the FPS only when you actually need on-screen/visual detail.
|
|
72
|
+
|
|
73
|
+
## Optimised for low token usage
|
|
43
74
|
|
|
44
|
-
|
|
75
|
+
This setup is deliberately token-lean:
|
|
45
76
|
|
|
46
|
-
`
|
|
77
|
+
- **`/caveman` skill + caveman hook** — ultra-compressed replies that keep full technical accuracy while cutting chatter (~75% fewer tokens), with `lite`/`full`/`ultra` intensity levels.
|
|
78
|
+
- **Video → 0 FPS** — analyse video by audio transcription only unless you explicitly need visual frames, avoiding expensive frame tokens.
|
|
79
|
+
- **`transcript-search` over re-explaining** — recalls past decisions from your own history instead of re-deriving context.
|
|
80
|
+
- **Scoped, high-signal hooks** — `stop-verify` and `eslint-fix` only act on files you actually changed, not whole-repo sweeps.
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
- **`claude-video-vision`** — video analysis
|
|
82
|
+
## On a new machine — two one-time steps
|
|
50
83
|
|
|
51
|
-
|
|
84
|
+
The installer reproduces **100% of the configuration and behaviour**. Two things are *identity and secrets*, not config, and can never live in a public package — you set them once:
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
1. **Log into Claude Code** (your account — you'd do this on any new machine anyway).
|
|
87
|
+
2. **Give `claude-video-vision` your own API key** if you use video analysis.
|
|
54
88
|
|
|
55
|
-
|
|
89
|
+
## Intentionally not shipped
|
|
56
90
|
|
|
57
|
-
- `skipDangerousModePermissionPrompt` — I won't
|
|
58
|
-
- The video-FPS `UserPromptSubmit` hook — it depends on `python3` and an unbundled MCP server.
|
|
91
|
+
- `skipDangerousModePermissionPrompt` — this disables a safety confirmation. I won't flip that on your machine by default; enable it yourself if you want it.
|
|
59
92
|
|
|
60
93
|
## License
|
|
61
94
|
|
package/bin/install.js
CHANGED
|
@@ -44,28 +44,79 @@ function makeExecutable(filePath) {
|
|
|
44
44
|
fs.chmodSync(filePath, '755');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Is a command resolvable on PATH? (uses `command -v` via a shell so it sees
|
|
48
|
+
// the same PATH the user's tools do, not just the bare which builtin.)
|
|
49
|
+
function have(cmd) {
|
|
50
|
+
return spawnSync('bash', ['-lc', `command -v ${cmd}`], { encoding: 'utf8' }).status === 0;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
function hasBrew() {
|
|
54
|
+
return have('brew');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Install a Homebrew formula. `required:true` → return false on failure so the
|
|
58
|
+
// caller can abort; `required:false` → best-effort, warn and continue (the
|
|
59
|
+
// related feature just stays dormant until the user installs it).
|
|
60
|
+
function brewInstall(pkg, { required = false } = {}) {
|
|
61
|
+
if (have(pkg)) { ok(`${pkg} present`); return true; }
|
|
62
|
+
if (!hasBrew()) {
|
|
63
|
+
const m = `${pkg} not found and Homebrew not installed.`;
|
|
64
|
+
if (required) { fail(m); console.log(c.yellow(` Install Homebrew (https://brew.sh) then re-run, or install ${pkg} manually.`)); return false; }
|
|
65
|
+
warn(`${m} Skipping — install it later for the related feature.`);
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
59
|
-
console.log(c.yellow(
|
|
60
|
-
const r = spawnSync('brew', ['install',
|
|
68
|
+
console.log(c.yellow(` Installing ${pkg} via Homebrew...`));
|
|
69
|
+
const r = spawnSync('brew', ['install', pkg], { stdio: 'inherit' });
|
|
61
70
|
if (r.status !== 0) {
|
|
62
|
-
fail(
|
|
71
|
+
if (required) { fail(`brew install ${pkg} failed. Install manually.`); return false; }
|
|
72
|
+
warn(`brew install ${pkg} failed — continuing without it.`);
|
|
63
73
|
return false;
|
|
64
74
|
}
|
|
65
|
-
ok(
|
|
75
|
+
ok(`${pkg} installed`);
|
|
66
76
|
return true;
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
// Register the MCP servers via the official `claude` CLI (writes to user scope,
|
|
80
|
+
// the same place they already live). Idempotent: skip any server already
|
|
81
|
+
// registered. SECURITY: env is always {} — never serialize the user's real
|
|
82
|
+
// environment or read ~/.claude.json (it holds the OAuth token).
|
|
83
|
+
function registerMcpServers() {
|
|
84
|
+
if (!have('claude')) {
|
|
85
|
+
warn('`claude` CLI not on PATH — skipping MCP registration.');
|
|
86
|
+
info(' After installing Claude Code, register them manually:');
|
|
87
|
+
info(` claude mcp add-json transcript-search '{"type":"stdio","command":"python3","args":["${path.join(CLAUDE_DIR, 'transcript-search', 'rag_lite.py')}","serve"],"env":{}}' --scope user`);
|
|
88
|
+
info(" claude mcp add claude-video-vision --scope user -- npx claude-video-vision");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const existing = spawnSync('bash', ['-lc', 'claude mcp list'], { encoding: 'utf8', timeout: 60000 }).stdout || '';
|
|
92
|
+
const servers = [
|
|
93
|
+
{
|
|
94
|
+
name: 'transcript-search',
|
|
95
|
+
json: JSON.stringify({
|
|
96
|
+
type: 'stdio',
|
|
97
|
+
command: 'python3',
|
|
98
|
+
args: [path.join(CLAUDE_DIR, 'transcript-search', 'rag_lite.py'), 'serve'],
|
|
99
|
+
env: {},
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'claude-video-vision',
|
|
104
|
+
json: JSON.stringify({
|
|
105
|
+
type: 'stdio',
|
|
106
|
+
command: 'npx',
|
|
107
|
+
args: ['claude-video-vision'],
|
|
108
|
+
env: {},
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
for (const s of servers) {
|
|
113
|
+
if (existing.includes(s.name)) { info(`MCP ${s.name} already registered — skipped`); continue; }
|
|
114
|
+
const r = spawnSync('claude', ['mcp', 'add-json', s.name, s.json, '--scope', 'user'], { encoding: 'utf8', timeout: 60000 });
|
|
115
|
+
if (r.status === 0) ok(`MCP ${s.name} registered`);
|
|
116
|
+
else warn(`MCP ${s.name} registration failed: ${(r.stderr || '').trim() || 'unknown error'}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
69
120
|
function mergeSettings(existingPath, incomingPath) {
|
|
70
121
|
const existing = JSON.parse(fs.readFileSync(existingPath, 'utf8'));
|
|
71
122
|
const incoming = JSON.parse(fs.readFileSync(incomingPath, 'utf8'));
|
|
@@ -104,20 +155,22 @@ function mergeSettings(existingPath, incomingPath) {
|
|
|
104
155
|
function main() {
|
|
105
156
|
console.log(c.bold('\nclaude-code-setup — installing Antonio\'s Claude config\n'));
|
|
106
157
|
|
|
107
|
-
// 1.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
158
|
+
// 1. Dependencies
|
|
159
|
+
// jq is required (hooks parse JSON with it). python3 powers the
|
|
160
|
+
// transcript-search MCP server; ffmpeg powers claude-video-vision —
|
|
161
|
+
// both best-effort (feature stays dormant if absent).
|
|
162
|
+
console.log(c.bold('Dependencies:'));
|
|
163
|
+
if (!brewInstall('jq', { required: true })) process.exit(1);
|
|
164
|
+
brewInstall('python3');
|
|
165
|
+
brewInstall('ffmpeg');
|
|
115
166
|
|
|
116
167
|
// 2. Ensure dirs
|
|
117
168
|
ensureDir(CLAUDE_DIR);
|
|
118
169
|
ensureDir(path.join(CLAUDE_DIR, 'hooks'));
|
|
119
170
|
ensureDir(path.join(CLAUDE_DIR, 'skills'));
|
|
171
|
+
ensureDir(path.join(CLAUDE_DIR, 'skills', 'caveman'));
|
|
120
172
|
ensureDir(path.join(CLAUDE_DIR, 'commands'));
|
|
173
|
+
ensureDir(path.join(CLAUDE_DIR, 'transcript-search'));
|
|
121
174
|
|
|
122
175
|
// 3. Hooks
|
|
123
176
|
const hooks = ['caveman-activate.sh', 'check-dep.sh', 'eslint-fix.sh', 'notify-sound.sh', 'scan-secrets.sh', 'stop-verify.sh'];
|
|
@@ -127,16 +180,21 @@ function main() {
|
|
|
127
180
|
makeExecutable(dest);
|
|
128
181
|
}
|
|
129
182
|
|
|
130
|
-
// 4. Skills
|
|
183
|
+
// 4. Skills (mute/unmute + the caveman /caveman skill with intensity levels)
|
|
131
184
|
for (const s of ['mute.md', 'unmute.md']) {
|
|
132
185
|
copyFile(path.join(FILES_DIR, 'skills', s), path.join(CLAUDE_DIR, 'skills', s), { skipIfExists: true });
|
|
133
186
|
}
|
|
187
|
+
copyFile(path.join(FILES_DIR, 'skills', 'caveman', 'SKILL.md'), path.join(CLAUDE_DIR, 'skills', 'caveman', 'SKILL.md'), { skipIfExists: true });
|
|
134
188
|
|
|
135
189
|
// 4b. Commands (slash commands referenced by CLAUDE.md)
|
|
136
190
|
for (const cmd of ['check-dep.md', 'debug.md', 'scan-secrets.md']) {
|
|
137
191
|
copyFile(path.join(FILES_DIR, 'commands', cmd), path.join(CLAUDE_DIR, 'commands', cmd), { skipIfExists: true });
|
|
138
192
|
}
|
|
139
193
|
|
|
194
|
+
// 4c. transcript-search engine (the MCP server script — index.db is built
|
|
195
|
+
// per-user from their own ~/.claude/projects and is never shipped)
|
|
196
|
+
copyFile(path.join(FILES_DIR, 'transcript-search', 'rag_lite.py'), path.join(CLAUDE_DIR, 'transcript-search', 'rag_lite.py'));
|
|
197
|
+
|
|
140
198
|
// 5. Statusline
|
|
141
199
|
const statuslineDest = path.join(CLAUDE_DIR, 'statusline.sh');
|
|
142
200
|
copyFile(path.join(FILES_DIR, 'statusline.sh'), statuslineDest);
|
|
@@ -168,7 +226,14 @@ function main() {
|
|
|
168
226
|
copyFile(path.join(FILES_DIR, 'settings.json'), settingsDest);
|
|
169
227
|
}
|
|
170
228
|
|
|
171
|
-
|
|
229
|
+
// 8. MCP servers (transcript-search + claude-video-vision)
|
|
230
|
+
console.log(c.bold('\nMCP servers:'));
|
|
231
|
+
registerMcpServers();
|
|
232
|
+
|
|
233
|
+
console.log(c.bold(c.green('\nDone. Restart Claude Code to apply.')));
|
|
234
|
+
console.log(c.dim('Two one-time steps on a new machine (identity, not config — they can\'t ship):'));
|
|
235
|
+
console.log(c.dim(' 1. Log into Claude Code.'));
|
|
236
|
+
console.log(c.dim(' 2. For video analysis, give claude-video-vision your own API key.\n'));
|
|
172
237
|
}
|
|
173
238
|
|
|
174
239
|
main();
|
package/files/settings.json
CHANGED
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"Bash(sw_vers:*)",
|
|
33
33
|
"Bash(date)",
|
|
34
34
|
"Bash(npm view:*)",
|
|
35
|
-
"Bash(npm ls:*)"
|
|
35
|
+
"Bash(npm ls:*)",
|
|
36
|
+
"Bash(claude update *)",
|
|
37
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({k: d.get\\(k\\) for k in ['dependencies','devDependencies']}, indent=2\\)\\)\")",
|
|
38
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('mcpServers', {}\\), indent=2\\)\\)\")"
|
|
36
39
|
]
|
|
37
40
|
},
|
|
38
41
|
"model": "sonnet",
|
|
@@ -117,6 +120,17 @@
|
|
|
117
120
|
]
|
|
118
121
|
}
|
|
119
122
|
],
|
|
123
|
+
"UserPromptSubmit": [
|
|
124
|
+
{
|
|
125
|
+
"hooks": [
|
|
126
|
+
{
|
|
127
|
+
"type": "command",
|
|
128
|
+
"command": "python3 -c \"\nimport json, sys, re\ndata = json.load(sys.stdin)\nmsg = data.get('message', '')\nis_video = (bool(re.search(r'(?i)(analys[ei]|watch|summariz|process|review|transcript)', msg)) and bool(re.search(r'(?i)(\\\\.(mp4|mov|avi|mkv|webm)|youtube\\\\.com|youtu\\\\.be|video)', msg))) or bool(re.search(r'https?://(www\\\\.)?(youtube\\\\.com/watch|youtu\\\\.be/)', msg))\nhas_fps = bool(re.search(r'(?i)(\\\\bfps\\\\b|\\\\d+\\\\s*fps|frame.{0,2}rate|frames.{0,3}per.{0,3}second)', msg))\nif is_video and not has_fps:\n print(json.dumps({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit', 'additionalContext': 'REMINDER: User wants to analyse a video but has NOT specified FPS. Before calling any video tools, ask them: How many FPS would you like to use? (default: auto)'}}))\n\"",
|
|
129
|
+
"timeout": 5
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
],
|
|
120
134
|
"Stop": [
|
|
121
135
|
{
|
|
122
136
|
"hooks": [
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: >
|
|
4
|
+
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
|
5
|
+
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
|
6
|
+
wenyan-lite, wenyan-full, wenyan-ultra.
|
|
7
|
+
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
|
8
|
+
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
12
|
+
|
|
13
|
+
## Persistence
|
|
14
|
+
|
|
15
|
+
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
|
16
|
+
|
|
17
|
+
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
|
22
|
+
|
|
23
|
+
Pattern: `[thing] [action] [reason]. [next step].`
|
|
24
|
+
|
|
25
|
+
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
26
|
+
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
27
|
+
|
|
28
|
+
## Intensity
|
|
29
|
+
|
|
30
|
+
| Level | What change |
|
|
31
|
+
|-------|------------|
|
|
32
|
+
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
|
33
|
+
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
|
34
|
+
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
|
|
35
|
+
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
|
36
|
+
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
|
37
|
+
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
|
38
|
+
|
|
39
|
+
Example — "Why React component re-render?"
|
|
40
|
+
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
|
41
|
+
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
|
42
|
+
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
|
43
|
+
|
|
44
|
+
Example — "Explain database connection pooling."
|
|
45
|
+
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
|
46
|
+
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
|
47
|
+
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
|
48
|
+
|
|
49
|
+
## Auto-Clarity
|
|
50
|
+
|
|
51
|
+
Drop caveman when:
|
|
52
|
+
- Security warnings
|
|
53
|
+
- Irreversible action confirmations
|
|
54
|
+
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
|
55
|
+
- Compression itself creates technical ambiguity
|
|
56
|
+
- User asks to clarify or repeats question
|
|
57
|
+
|
|
58
|
+
Resume caveman after clear part done.
|
|
59
|
+
|
|
60
|
+
## Boundaries
|
|
61
|
+
|
|
62
|
+
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lightweight transcript search for Claude Code.
|
|
3
|
+
|
|
4
|
+
SQLite FTS5 full-text index over past conversation transcripts in
|
|
5
|
+
~/.claude/projects/**/*.jsonl. No embeddings, no daemons, no extra deps —
|
|
6
|
+
pure Python stdlib. Indexes human prompts + assistant text only (tool output,
|
|
7
|
+
thinking, and system noise are skipped to keep the signal high).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 rag_lite.py index # build / refresh the index
|
|
11
|
+
python3 rag_lite.py search "query" [n] # search from the CLI
|
|
12
|
+
python3 rag_lite.py stats # row / file counts
|
|
13
|
+
python3 rag_lite.py serve # run as an MCP stdio server
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import glob
|
|
19
|
+
import sqlite3
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
HOME = os.path.expanduser("~")
|
|
23
|
+
PROJ_DIR = os.path.join(HOME, ".claude", "projects")
|
|
24
|
+
DB_DIR = os.path.join(HOME, ".claude", "transcript-search")
|
|
25
|
+
DB_PATH = os.path.join(DB_DIR, "index.db")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------- storage
|
|
29
|
+
def db():
|
|
30
|
+
os.makedirs(DB_DIR, exist_ok=True)
|
|
31
|
+
conn = sqlite3.connect(DB_PATH)
|
|
32
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
33
|
+
conn.execute(
|
|
34
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS chunks USING fts5("
|
|
35
|
+
"text, role UNINDEXED, session UNINDEXED, project UNINDEXED, "
|
|
36
|
+
"branch UNINDEXED, ts UNINDEXED, file UNINDEXED, line UNINDEXED, "
|
|
37
|
+
"tokenize='porter unicode61')"
|
|
38
|
+
)
|
|
39
|
+
conn.execute(
|
|
40
|
+
"CREATE TABLE IF NOT EXISTS files("
|
|
41
|
+
"path TEXT PRIMARY KEY, size INTEGER, mtime REAL, lines INTEGER)"
|
|
42
|
+
)
|
|
43
|
+
return conn
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------- parsing
|
|
47
|
+
def extract(obj):
|
|
48
|
+
"""Return (role, text) for indexable turns, else None.
|
|
49
|
+
|
|
50
|
+
Indexes: user string prompts, assistant text blocks.
|
|
51
|
+
Skips: tool_use / tool_result payloads, thinking, and everything else.
|
|
52
|
+
"""
|
|
53
|
+
t = obj.get("type")
|
|
54
|
+
if t not in ("user", "assistant"):
|
|
55
|
+
return None
|
|
56
|
+
m = obj.get("message") or {}
|
|
57
|
+
role = m.get("role") or t
|
|
58
|
+
cont = m.get("content")
|
|
59
|
+
parts = []
|
|
60
|
+
if isinstance(cont, str):
|
|
61
|
+
if role != "user":
|
|
62
|
+
return None
|
|
63
|
+
parts.append(cont)
|
|
64
|
+
elif isinstance(cont, list):
|
|
65
|
+
if role != "assistant":
|
|
66
|
+
return None # list content on a user turn = tool results -> skip
|
|
67
|
+
for b in cont:
|
|
68
|
+
if isinstance(b, dict) and b.get("type") == "text" and b.get("text"):
|
|
69
|
+
parts.append(b["text"])
|
|
70
|
+
text = "\n".join(p for p in parts if p).strip()
|
|
71
|
+
if not text:
|
|
72
|
+
return None
|
|
73
|
+
# Drop local-command / system-reminder wrappers — pure noise for recall.
|
|
74
|
+
if text.startswith("<local-command") or text.startswith("Caveat:"):
|
|
75
|
+
return None
|
|
76
|
+
return role, text[:20000]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------- indexing
|
|
80
|
+
def index_file(conn, path):
|
|
81
|
+
st = os.stat(path)
|
|
82
|
+
row = conn.execute(
|
|
83
|
+
"SELECT size, mtime, lines FROM files WHERE path=?", (path,)
|
|
84
|
+
).fetchone()
|
|
85
|
+
start = 0
|
|
86
|
+
if row:
|
|
87
|
+
size, mtime, lines = row
|
|
88
|
+
if size == st.st_size and mtime == st.st_mtime:
|
|
89
|
+
return 0 # unchanged
|
|
90
|
+
if st.st_size >= size:
|
|
91
|
+
start = lines # append-only growth: skip already-indexed lines
|
|
92
|
+
else:
|
|
93
|
+
conn.execute("DELETE FROM chunks WHERE file=?", (path,)) # rewritten
|
|
94
|
+
added = 0
|
|
95
|
+
ln = -1
|
|
96
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
97
|
+
for ln, line in enumerate(fh):
|
|
98
|
+
if ln < start:
|
|
99
|
+
continue
|
|
100
|
+
line = line.strip()
|
|
101
|
+
if not line:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
obj = json.loads(line)
|
|
105
|
+
except Exception:
|
|
106
|
+
continue
|
|
107
|
+
ex = extract(obj)
|
|
108
|
+
if not ex:
|
|
109
|
+
continue
|
|
110
|
+
role, text = ex
|
|
111
|
+
cwd = obj.get("cwd") or ""
|
|
112
|
+
project = os.path.basename(cwd) if cwd else os.path.basename(os.path.dirname(path))
|
|
113
|
+
conn.execute(
|
|
114
|
+
"INSERT INTO chunks(text,role,session,project,branch,ts,file,line) "
|
|
115
|
+
"VALUES(?,?,?,?,?,?,?,?)",
|
|
116
|
+
(text, role, obj.get("sessionId") or os.path.basename(path),
|
|
117
|
+
project, obj.get("gitBranch") or "", obj.get("timestamp") or "",
|
|
118
|
+
path, ln),
|
|
119
|
+
)
|
|
120
|
+
added += 1
|
|
121
|
+
conn.execute(
|
|
122
|
+
"INSERT INTO files(path,size,mtime,lines) VALUES(?,?,?,?) "
|
|
123
|
+
"ON CONFLICT(path) DO UPDATE SET size=excluded.size, mtime=excluded.mtime, lines=excluded.lines",
|
|
124
|
+
(path, st.st_size, st.st_mtime, ln + 1),
|
|
125
|
+
)
|
|
126
|
+
return added
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def index_all(conn):
|
|
130
|
+
total = 0
|
|
131
|
+
for path in glob.glob(os.path.join(PROJ_DIR, "*", "*.jsonl")):
|
|
132
|
+
try:
|
|
133
|
+
total += index_file(conn, path)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
conn.commit()
|
|
137
|
+
return total
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------- search
|
|
141
|
+
def fts_query(q):
|
|
142
|
+
toks = re.findall(r"[\w']+", q or "")
|
|
143
|
+
toks = [t for t in toks if len(t) > 1 or t.isdigit()]
|
|
144
|
+
if not toks:
|
|
145
|
+
return None
|
|
146
|
+
return " OR ".join('"%s"' % t.replace('"', '""') for t in toks)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def search(conn, query, limit=8, project=None):
|
|
150
|
+
fq = fts_query(query)
|
|
151
|
+
if not fq:
|
|
152
|
+
return []
|
|
153
|
+
sql = ("SELECT snippet(chunks,0,'»','«',' … ',16), role, session, project, "
|
|
154
|
+
"branch, ts, file, line FROM chunks WHERE chunks MATCH ?")
|
|
155
|
+
args = [fq]
|
|
156
|
+
if project:
|
|
157
|
+
sql += " AND project=?"
|
|
158
|
+
args.append(project)
|
|
159
|
+
sql += " ORDER BY bm25(chunks) LIMIT ?"
|
|
160
|
+
args.append(int(limit))
|
|
161
|
+
return conn.execute(sql, args).fetchall()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_context(path, line, before=3, after=3):
|
|
165
|
+
lo, hi = max(0, int(line) - before), int(line) + after
|
|
166
|
+
out = []
|
|
167
|
+
try:
|
|
168
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
169
|
+
for i, l in enumerate(fh):
|
|
170
|
+
if i < lo:
|
|
171
|
+
continue
|
|
172
|
+
if i > hi:
|
|
173
|
+
break
|
|
174
|
+
try:
|
|
175
|
+
o = json.loads(l)
|
|
176
|
+
except Exception:
|
|
177
|
+
continue
|
|
178
|
+
ex = extract(o)
|
|
179
|
+
if ex:
|
|
180
|
+
out.append((i, ex[0], ex[1][:1800]))
|
|
181
|
+
except OSError:
|
|
182
|
+
pass
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def fmt_results(rows):
|
|
187
|
+
if not rows:
|
|
188
|
+
return "No matches."
|
|
189
|
+
out = []
|
|
190
|
+
for snip, role, _session, project, branch, ts, path, line in rows:
|
|
191
|
+
date = (ts or "")[:10] or "?"
|
|
192
|
+
br = branch or "-"
|
|
193
|
+
snip = re.sub(r"\s+", " ", snip).strip()
|
|
194
|
+
out.append(f"[{project} · {br} · {date} · {role}] {path}:{line}\n {snip}")
|
|
195
|
+
out.append("\nUse get_context(file, line) for the full surrounding turns.")
|
|
196
|
+
return "\n\n".join(out)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def fmt_context(rows):
|
|
200
|
+
if not rows:
|
|
201
|
+
return "No context found at that location."
|
|
202
|
+
return "\n\n".join(f"--- line {i} ({role}) ---\n{text}" for i, role, text in rows)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------- MCP server
|
|
206
|
+
TOOLS = [
|
|
207
|
+
{
|
|
208
|
+
"name": "search",
|
|
209
|
+
"description": (
|
|
210
|
+
"Full-text search over past Claude Code conversation transcripts "
|
|
211
|
+
"(human prompts + assistant replies) for this and other projects. "
|
|
212
|
+
"Use when the user references past work ('we talked about', 'remember "
|
|
213
|
+
"when', 'like last time') or you need a prior decision/context."
|
|
214
|
+
),
|
|
215
|
+
"inputSchema": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"properties": {
|
|
218
|
+
"query": {"type": "string", "description": "Search terms."},
|
|
219
|
+
"limit": {"type": "integer", "description": "Max results (default 8)."},
|
|
220
|
+
"project": {"type": "string", "description": "Optional: restrict to a project folder name."},
|
|
221
|
+
},
|
|
222
|
+
"required": ["query"],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "get_context",
|
|
227
|
+
"description": "Return the surrounding conversation turns around a transcript hit (use the file+line from a search result).",
|
|
228
|
+
"inputSchema": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"properties": {
|
|
231
|
+
"file": {"type": "string"},
|
|
232
|
+
"line": {"type": "integer"},
|
|
233
|
+
"before": {"type": "integer"},
|
|
234
|
+
"after": {"type": "integer"},
|
|
235
|
+
},
|
|
236
|
+
"required": ["file", "line"],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _send(obj):
|
|
243
|
+
sys.stdout.write(json.dumps(obj) + "\n")
|
|
244
|
+
sys.stdout.flush()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _reply(mid, result):
|
|
248
|
+
_send({"jsonrpc": "2.0", "id": mid, "result": result})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _error(mid, code, message):
|
|
252
|
+
_send({"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}})
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def serve():
|
|
256
|
+
conn = db()
|
|
257
|
+
try:
|
|
258
|
+
index_all(conn)
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
for raw in sys.stdin:
|
|
262
|
+
raw = raw.strip()
|
|
263
|
+
if not raw:
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
msg = json.loads(raw)
|
|
267
|
+
except Exception:
|
|
268
|
+
continue
|
|
269
|
+
method = msg.get("method")
|
|
270
|
+
mid = msg.get("id")
|
|
271
|
+
if method == "initialize":
|
|
272
|
+
ver = (msg.get("params") or {}).get("protocolVersion", "2025-06-18")
|
|
273
|
+
_reply(mid, {
|
|
274
|
+
"protocolVersion": ver,
|
|
275
|
+
"capabilities": {"tools": {}},
|
|
276
|
+
"serverInfo": {"name": "transcript-search", "version": "1.0.0"},
|
|
277
|
+
})
|
|
278
|
+
elif method in ("notifications/initialized", "notifications/cancelled"):
|
|
279
|
+
continue # notifications: no response
|
|
280
|
+
elif method == "ping":
|
|
281
|
+
_reply(mid, {})
|
|
282
|
+
elif method == "tools/list":
|
|
283
|
+
_reply(mid, {"tools": TOOLS})
|
|
284
|
+
elif method == "tools/call":
|
|
285
|
+
p = msg.get("params") or {}
|
|
286
|
+
name = p.get("name")
|
|
287
|
+
a = p.get("arguments") or {}
|
|
288
|
+
try:
|
|
289
|
+
if name == "search":
|
|
290
|
+
index_all(conn)
|
|
291
|
+
text = fmt_results(search(conn, a.get("query", ""),
|
|
292
|
+
a.get("limit", 8), a.get("project")))
|
|
293
|
+
elif name == "get_context":
|
|
294
|
+
text = fmt_context(get_context(a.get("file", ""), a.get("line", 0),
|
|
295
|
+
a.get("before", 3), a.get("after", 3)))
|
|
296
|
+
else:
|
|
297
|
+
_error(mid, -32601, "unknown tool: %s" % name)
|
|
298
|
+
continue
|
|
299
|
+
_reply(mid, {"content": [{"type": "text", "text": text}]})
|
|
300
|
+
except Exception as e:
|
|
301
|
+
_reply(mid, {"content": [{"type": "text", "text": "error: %s" % e}], "isError": True})
|
|
302
|
+
elif mid is not None:
|
|
303
|
+
_error(mid, -32601, "method not found: %s" % method)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------- CLI
|
|
307
|
+
def main():
|
|
308
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "serve"
|
|
309
|
+
if cmd == "serve":
|
|
310
|
+
serve()
|
|
311
|
+
elif cmd == "index":
|
|
312
|
+
conn = db()
|
|
313
|
+
n = index_all(conn)
|
|
314
|
+
print("indexed %d new turns" % n)
|
|
315
|
+
elif cmd == "stats":
|
|
316
|
+
conn = db()
|
|
317
|
+
index_all(conn)
|
|
318
|
+
rows = conn.execute("SELECT COUNT(*) FROM chunks").fetchone()[0]
|
|
319
|
+
files = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]
|
|
320
|
+
projs = conn.execute("SELECT project, COUNT(*) FROM chunks GROUP BY project ORDER BY 2 DESC").fetchall()
|
|
321
|
+
print("turns: %d files: %d" % (rows, files))
|
|
322
|
+
for pr, c in projs:
|
|
323
|
+
print(" %5d %s" % (c, pr))
|
|
324
|
+
elif cmd == "search":
|
|
325
|
+
conn = db()
|
|
326
|
+
index_all(conn)
|
|
327
|
+
q = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
328
|
+
n = int(sys.argv[3]) if len(sys.argv) > 3 else 8
|
|
329
|
+
print(fmt_results(search(conn, q, n)))
|
|
330
|
+
else:
|
|
331
|
+
print(__doc__)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antonior/claude-code-setup",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Install Antonio's Claude Code
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Install Antonio's full Claude Code setup: hooks, slash commands, skills, statusline, settings, and MCP servers (transcript-search + video-vision)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-code-setup": "bin/install.js"
|
|
7
7
|
},
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
"claude-code",
|
|
15
15
|
"anthropic",
|
|
16
16
|
"config",
|
|
17
|
-
"setup"
|
|
17
|
+
"setup",
|
|
18
|
+
"mcp",
|
|
19
|
+
"hooks",
|
|
20
|
+
"dotfiles"
|
|
18
21
|
],
|
|
19
22
|
"author": "Antonio Radosav <antonio.radosav@protonmail.com>",
|
|
20
23
|
"license": "MIT",
|