@bglocation/tune-context 1.0.0
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +20 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/agents/explore-cheap.md +18 -0
- package/agents/reviewer.md +18 -0
- package/bin/tune-context.mjs +89 -0
- package/cli/detect.mjs +114 -0
- package/cli/generate.mjs +123 -0
- package/cli/managed-block.mjs +26 -0
- package/cli/sync-doctrine.mjs +29 -0
- package/cli/verify.mjs +52 -0
- package/eval/adoption-report.mjs +244 -0
- package/hooks/adoption-log.mjs +132 -0
- package/hooks/hooks.json +48 -0
- package/hooks/pre-compact-snapshot.mjs +73 -0
- package/hooks/session-start-reinject.mjs +40 -0
- package/package.json +45 -0
- package/skills/caveman/SKILL.md +26 -0
- package/skills/cdd/SKILL.md +50 -0
- package/skills/phase-workflow/SKILL.md +28 -0
- package/skills/token-efficiency/SKILL.md +52 -0
- package/skills/tune-context/SKILL.md +157 -0
- package/skills/tune-context/templates/CLAUDE.md.project.tmpl +39 -0
- package/skills/tune-context/templates/CLAUDE.md.user.tmpl +23 -0
- package/skills/tune-context/templates/settings.json.tmpl +58 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bglocation",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Szymon Walczak",
|
|
5
|
+
"url": "https://bglocation.dev"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "tune-context",
|
|
10
|
+
"source": "./",
|
|
11
|
+
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
12
|
+
"version": "1.0.0",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Szymon Walczak",
|
|
15
|
+
"url": "https://bglocation.dev"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tune-context",
|
|
3
|
+
"displayName": "tune-context",
|
|
4
|
+
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Szymon Walczak",
|
|
8
|
+
"url": "https://bglocation.dev"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://bglocation.dev",
|
|
11
|
+
"repository": "https://gitlab.com/bglocation/tune-context",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"context-engineering",
|
|
16
|
+
"token-efficiency",
|
|
17
|
+
"configurator",
|
|
18
|
+
"skills"
|
|
19
|
+
]
|
|
20
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Szymon Walczak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# tune-context
|
|
2
|
+
|
|
3
|
+
Context-efficiency configurator for Claude Code — the doctrine (token-saving
|
|
4
|
+
working methods) packaged as **skills**, plus a `tune-context` configurator that
|
|
5
|
+
scaffolds a cost-optimal, high-quality setup (lean CLAUDE.md + skills + subagents
|
|
6
|
+
+ settings + hooks + MCP).
|
|
7
|
+
|
|
8
|
+
**Self-contained** — zero dependency on any host repo. Ships two ways: a Claude
|
|
9
|
+
Code **plugin** and an **npm CLI** (`tune-context init`), both under
|
|
10
|
+
`@bglocation`. Incubated inside the [`code-search-mcp`](https://gitlab.com/bglocation/code-search-mcp)
|
|
11
|
+
project, now extracted to its own repository.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
tune-context/
|
|
15
|
+
├── .claude-plugin/plugin.json # Claude Code plugin manifest
|
|
16
|
+
├── skills/ # token-efficiency, caveman, cdd, phase-workflow, tune-context (+ templates/)
|
|
17
|
+
├── agents/ # explore-cheap, reviewer (model right-sizing)
|
|
18
|
+
├── hooks/ # PreCompact state-doc, SessionStart re-inject, PostToolUse adoption log + hooks.json
|
|
19
|
+
├── bin/ + cli/ # `tune-context init` — deterministic detect → generate → verify
|
|
20
|
+
├── eval/ # adoption-report.mjs (the rag-usage analog)
|
|
21
|
+
└── scripts/ # verify-pack, smoke-init, sync-tune-context (dev/maintenance)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Node ≥ 18, no build step. From the repo you want to configure:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Configure this repo — deterministic, no Claude session needed
|
|
30
|
+
npx -p @bglocation/tune-context tune-context init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> The `-p <pkg> tune-context` form is deliberate: the package is **scoped**
|
|
34
|
+
> (`@bglocation/…`) and its bin (`tune-context`) doesn't match the scope, so a
|
|
35
|
+
> bare `npx @bglocation/tune-context` can fail to resolve the command. `-p`
|
|
36
|
+
> installs the package and names the bin explicitly.
|
|
37
|
+
|
|
38
|
+
That single command detects the stack + MCP servers, writes a lean `CLAUDE.md`,
|
|
39
|
+
syncs the doctrine skills/agents/hooks into `~/.claude/`, merges hooks and
|
|
40
|
+
permissions, and prints a verification report. It's idempotent — safe to re-run.
|
|
41
|
+
|
|
42
|
+
Install it globally if you'll use it across repos:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @bglocation/tune-context
|
|
46
|
+
tune-context init # in any repo
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Prefer the model-driven path? Install the plugin and use the skill — it does the
|
|
50
|
+
same thing plus wiki wiring and richer MCP detection:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude --plugin-dir "$(npm root -g)/@bglocation/tune-context" # global install
|
|
54
|
+
# then, inside a session in the repo you want to configure:
|
|
55
|
+
/tune-context
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The section below breaks down all three entry points and when to use each.
|
|
59
|
+
|
|
60
|
+
## Install & run
|
|
61
|
+
|
|
62
|
+
Three entry points, same doctrine. Pick whichever fits how you work:
|
|
63
|
+
|
|
64
|
+
**1. `/tune-context` skill (recommended).** With the plugin installed (below), run
|
|
65
|
+
`/tune-context` in any repo. It detects the stack, MCP servers and existing config,
|
|
66
|
+
then generates a lean project `CLAUDE.md`, wires the doctrine skills, and merges
|
|
67
|
+
hooks + permissions — idempotently, never clobbering hand-written content. This is
|
|
68
|
+
the full-fidelity path (it also handles wiki wiring and semantic-search detection).
|
|
69
|
+
|
|
70
|
+
**2. CLI `tune-context init`.** The deterministic, no-model twin of the skill — same
|
|
71
|
+
managed-block rules, same never-clobber guarantee. Good for CI or scripted setup:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx -p @bglocation/tune-context tune-context init # runs in the current repo
|
|
75
|
+
npx -p @bglocation/tune-context tune-context init --cwd . # explicit target dir
|
|
76
|
+
# from a checkout (dev):
|
|
77
|
+
node bin/tune-context.mjs init --cwd /path/to/your/repo
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It prints what it detected, what it synced/merged, and a verification report; a
|
|
81
|
+
second run is a no-op.
|
|
82
|
+
|
|
83
|
+
**3. Plugin install.** Load the plugin so the skills, agents and hooks are available
|
|
84
|
+
in every session. The best UX is the **marketplace** — install once, get updates
|
|
85
|
+
with `/plugin marketplace update`:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
/plugin marketplace add https://gitlab.com/bglocation/tune-context
|
|
89
|
+
/plugin install tune-context@bglocation
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or point Claude at a directory directly (no marketplace):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
claude --plugin-dir "$(npm root -g)/@bglocation/tune-context" # global npm install
|
|
96
|
+
claude --plugin-dir /path/to/tune-context # or a local checkout
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The plugin manifest (`.claude-plugin/plugin.json`) and hook wiring
|
|
100
|
+
(`hooks/hooks.json`, via `${CLAUDE_PLUGIN_ROOT}`) register the four hook events
|
|
101
|
+
automatically; `bin/tune-context` is added to `PATH` while the plugin is enabled.
|
|
102
|
+
The marketplace catalog is [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json).
|
|
103
|
+
|
|
104
|
+
## Measuring adoption (does the doctrine actually get used?)
|
|
105
|
+
`hooks/adoption-log.mjs` logs one JSONL line per event to
|
|
106
|
+
`~/.claude/tune-context/adoption.jsonl`, and `eval/adoption-report.mjs` turns that
|
|
107
|
+
into a report: RAG vs grep, doctrine skills, subagents, re-read churn — with a
|
|
108
|
+
verdict per lever. It's registered under two Claude Code hook events —
|
|
109
|
+
`PostToolUse` (every tool call) and `SubagentStart` (subagent spawns, which aren't
|
|
110
|
+
regular tools).
|
|
111
|
+
|
|
112
|
+
### Turn it on
|
|
113
|
+
|
|
114
|
+
1. **Install the hook.** Either run **`/tune-context`** in your repo (it copies the
|
|
115
|
+
script to `~/.claude/hooks/` and merges the hook entries into `settings.json`
|
|
116
|
+
for you), or do it by hand:
|
|
117
|
+
- copy `hooks/adoption-log.mjs` → `~/.claude/hooks/adoption-log.mjs` (needs
|
|
118
|
+
Node ≥ 18 on `PATH`);
|
|
119
|
+
- add the `PostToolUse` **and** `SubagentStart` blocks from
|
|
120
|
+
[`skills/tune-context/templates/settings.json.tmpl`](skills/tune-context/templates/settings.json.tmpl)
|
|
121
|
+
to your `settings.json` — `~/.claude/settings.json` for every project, or a
|
|
122
|
+
repo's `.claude/settings.json` for just that one.
|
|
123
|
+
|
|
124
|
+
2. **No reload needed.** Claude Code watches the settings files, so the new hooks
|
|
125
|
+
go live within a few seconds of saving — **you do not need to reload VSCode,
|
|
126
|
+
restart the extension, or start a new session.** Only if nothing shows up after
|
|
127
|
+
a few seconds, restart the session to force a reload. Check what's active with
|
|
128
|
+
the `/hooks` command.
|
|
129
|
+
|
|
130
|
+
3. **Just work.** Every tool call and subagent run now appends a line to
|
|
131
|
+
`~/.claude/tune-context/adoption.jsonl` (created on first write).
|
|
132
|
+
|
|
133
|
+
4. **Read the report** any time, from this checkout (or the installed package):
|
|
134
|
+
```bash
|
|
135
|
+
node eval/adoption-report.mjs # default log location
|
|
136
|
+
node eval/adoption-report.mjs --log <path>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Stop / opt out:** delete the `PostToolUse` + `SubagentStart` entries from
|
|
140
|
+
`settings.json` (watcher picks that up too). The log stays local under
|
|
141
|
+
`~/.claude/tune-context/` and records only tool names, file paths, and
|
|
142
|
+
skill/agent names — never full Bash command strings.
|
|
143
|
+
|
|
144
|
+
**Honest scope:** this is a *behavioral proxy*, not a bill. Token cost lands only
|
|
145
|
+
in OTEL (`CLAUDE_CODE_ENABLE_TELEMETRY=1` → `claude_code.cost.usage`, needs a
|
|
146
|
+
collector), never a hook; and caveman is an output style with no tool call, so it
|
|
147
|
+
can't be counted this way.
|
|
148
|
+
|
|
149
|
+
### Getting real cost (OTEL) — outside tune-context
|
|
150
|
+
|
|
151
|
+
tune-context does not set this up for you (deliberate — see TASK-062: it needs
|
|
152
|
+
infra this tool stays zero-infra without). If you want actual dollars instead of
|
|
153
|
+
the behavioral proxy above:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export CLAUDE_CODE_ENABLE_TELEMETRY=1 # off by default
|
|
157
|
+
export OTEL_METRICS_EXPORTER=prometheus # NOT console — console is CLI-only and
|
|
158
|
+
# conflicts with the SDK/message stream
|
|
159
|
+
# a VSCode-extension session may use
|
|
160
|
+
export OTEL_METRIC_EXPORT_INTERVAL=1000 # default 60000ms; short sessions can
|
|
161
|
+
# exit before ever exporting
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Start a new Claude Code session with those set (must be inherited at launch —
|
|
165
|
+
e.g. exported in the shell profile before opening VSCode, not just typed into an
|
|
166
|
+
in-editor terminal), work for a bit, then:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
curl -s http://localhost:9464/metrics | grep claude_code
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Look for **`claude_code.cost.usage`** (USD, carries `session.id`) and
|
|
173
|
+
**`claude_code.token.usage`** (token counts). No collector needed just to see the
|
|
174
|
+
numbers — `curl` against the local Prometheus endpoint is enough; wire a real
|
|
175
|
+
Prometheus/Grafana at that endpoint only if you want history instead of a
|
|
176
|
+
point-in-time read. Traces (`OTEL_TRACES_EXPORTER=otlp`) need an extra
|
|
177
|
+
`CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1` flag and aren't needed just for cost.
|
|
178
|
+
|
|
179
|
+
## Design principles
|
|
180
|
+
- **Minimal always-on**, depth on-demand: thin CLAUDE.md points at skills; skill
|
|
181
|
+
*bodies* load only when invoked (progressive disclosure).
|
|
182
|
+
- **`@import` is not a cost lever** — it is inlined in full. Reduce via skills.
|
|
183
|
+
- **Right-size the model per role** (subagent `model:`).
|
|
184
|
+
- **Caveman targets output** (uncached, ~4–5× input); skills/subagents target the
|
|
185
|
+
window.
|
|
186
|
+
|
|
187
|
+
## Local dogfooding
|
|
188
|
+
`node scripts/sync-tune-context.mjs` mirrors `skills/`, `agents/` and `hooks/`
|
|
189
|
+
into `~/.claude/` (hooks get `chmod 755`). Source of truth stays here. Skills take
|
|
190
|
+
effect on the next Claude Code session; hooks are hot-reloaded by the settings
|
|
191
|
+
file watcher (once their `settings.json` entries exist).
|
|
192
|
+
|
|
193
|
+
## Related — the `@bglocation` toolbelt
|
|
194
|
+
- **[`@bglocation/code-search-mcp`](https://gitlab.com/bglocation/code-search-mcp)**
|
|
195
|
+
— semantic code index (RAG) + MCP server. The strongest lever tune-context
|
|
196
|
+
detects and wires: search by meaning instead of `grep`, read only the returned
|
|
197
|
+
spans. tune-context runs fine without it, but recommends it when no
|
|
198
|
+
semantic-search MCP is present.
|
|
199
|
+
- **[bglocation.dev](https://bglocation.dev)** — the rest of the toolbelt.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explore-cheap
|
|
3
|
+
description: Cheap read-only fan-out for broad code/file searches — "where is everything about X", naming-convention hunts, locating code across many files. Returns a tight conclusion, not raw dumps. Use instead of grepping many files in the main session.
|
|
4
|
+
tools: Read, Grep, Glob
|
|
5
|
+
model: haiku
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# explore-cheap
|
|
9
|
+
|
|
10
|
+
Read-only search fan-out on a cheap model. You sweep many files and return a
|
|
11
|
+
tight conclusion — the main session pays for the answer, not the search.
|
|
12
|
+
|
|
13
|
+
- Locate code/config across the repo; map naming conventions; find all call sites.
|
|
14
|
+
- Read excerpts, not whole files. Report `path:line` references plus a short synthesis.
|
|
15
|
+
- **Read-only:** never edit, run builds, or change anything.
|
|
16
|
+
- You do file-level search (grep/glob). Semantic "where/why is X" search is the
|
|
17
|
+
main session's job (`search_codebase`); you handle the exhaustive sweeps it
|
|
18
|
+
would rather not pull into its context.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Reviews a diff or a slice of code for correctness bugs and clear simplifications — judgment work, not mechanical search. Returns findings ranked by severity with rationale. Use for a second-pass review before committing non-trivial changes.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: fable
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# reviewer
|
|
9
|
+
|
|
10
|
+
Second-pass review on a strong model — this is judgment, so it is worth the
|
|
11
|
+
better model (right-sizing: cheap for search, strong for review). For the hardest
|
|
12
|
+
reviews, override to `opus` per invocation.
|
|
13
|
+
|
|
14
|
+
- Correctness bugs first, then clear simplifications / reuse.
|
|
15
|
+
- Read the diff and enough surrounding code to judge — don't rewrite, report.
|
|
16
|
+
- Return findings ranked by severity, each with a one-line rationale and a
|
|
17
|
+
`path:line` anchor.
|
|
18
|
+
- Prefer `git diff` over re-reading whole files.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tune-context init — deterministic detect -> generate -> verify.
|
|
3
|
+
// This is the CLI/plugin-bin twin of skills/tune-context/SKILL.md: same
|
|
4
|
+
// managed-block rules, same "never clobber" guarantee, no model in the loop.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { claudeDirs, detectExistingConfig, detectMcpServers, detectStack } from '../cli/detect.mjs';
|
|
9
|
+
import {
|
|
10
|
+
buildPermissionsAllow,
|
|
11
|
+
generateProjectClaudeMd,
|
|
12
|
+
generateUserClaudeMd,
|
|
13
|
+
installDoctrine,
|
|
14
|
+
mergeHooks,
|
|
15
|
+
mergePermissions,
|
|
16
|
+
} from '../cli/generate.mjs';
|
|
17
|
+
import { verify } from '../cli/verify.mjs';
|
|
18
|
+
|
|
19
|
+
const HOOK_EVENTS = [
|
|
20
|
+
{ event: 'PreCompact', scriptName: 'pre-compact-snapshot.mjs' },
|
|
21
|
+
{ event: 'SessionStart', scriptName: 'session-start-reinject.mjs' },
|
|
22
|
+
{ event: 'PostToolUse', scriptName: 'adoption-log.mjs' },
|
|
23
|
+
{ event: 'SubagentStart', scriptName: 'adoption-log.mjs' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function usage() {
|
|
27
|
+
console.log(`tune-context init — configure this repo for token-efficient Claude Code use.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
tune-context init [--cwd <dir>]
|
|
31
|
+
tune-context --help
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runInit(cwd) {
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const pkgRoot = resolve(here, '..');
|
|
38
|
+
const templatesDir = join(pkgRoot, 'skills', 'tune-context', 'templates');
|
|
39
|
+
const { claudeDir } = claudeDirs();
|
|
40
|
+
|
|
41
|
+
const stack = detectStack(cwd);
|
|
42
|
+
const existing = detectExistingConfig(cwd);
|
|
43
|
+
const mcpServers = detectMcpServers(cwd);
|
|
44
|
+
const detection = { stack, mcpServers };
|
|
45
|
+
|
|
46
|
+
const synced = installDoctrine(pkgRoot, claudeDir);
|
|
47
|
+
const userClaudeMd = generateUserClaudeMd(claudeDir, templatesDir);
|
|
48
|
+
const projectClaudeMd = generateProjectClaudeMd(cwd, detection, templatesDir);
|
|
49
|
+
|
|
50
|
+
const settingsTemplate = JSON.parse(readFileSync(join(templatesDir, 'settings.json.tmpl'), 'utf8'));
|
|
51
|
+
const hooksAdded = mergeHooks(existing.userSettingsJson, { hooks: settingsTemplate.hooks });
|
|
52
|
+
const permissionsAllow = buildPermissionsAllow(detection);
|
|
53
|
+
const permissionsAdded = mergePermissions(existing.projectSettingsJson, permissionsAllow);
|
|
54
|
+
|
|
55
|
+
const result = verify({
|
|
56
|
+
claudeDir,
|
|
57
|
+
userSettingsPath: existing.userSettingsJson,
|
|
58
|
+
projectSettingsPath: existing.projectSettingsJson,
|
|
59
|
+
expectedHookCommands: HOOK_EVENTS,
|
|
60
|
+
expectedPermissions: permissionsAllow,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`tune-context init — ${cwd}\n`);
|
|
64
|
+
console.log(`Stack: ${stack.ecosystem}${stack.name ? ` (${stack.name})` : ''}${stack.monorepo ? ', monorepo' : ''}`);
|
|
65
|
+
if (stack.undetectedCommands) {
|
|
66
|
+
console.log(' build/test/lint commands not detected for this ecosystem — filled with TODO placeholders in CLAUDE.md.');
|
|
67
|
+
}
|
|
68
|
+
console.log(`\nDoctrine synced to ${claudeDir}: ${synced.length} item(s)`);
|
|
69
|
+
console.log(`User CLAUDE.md: ${userClaudeMd.changed ? 'written' : `skipped (${userClaudeMd.skipped})`}`);
|
|
70
|
+
console.log(`Project CLAUDE.md: ${projectClaudeMd.changed ? `updated ${projectClaudeMd.path}` : 'already up to date'}`);
|
|
71
|
+
console.log(`Hooks merged into ${existing.userSettingsJson}: ${hooksAdded.length ? hooksAdded.join(', ') : 'already present'}`);
|
|
72
|
+
console.log(`Permissions merged into ${existing.projectSettingsJson}: ${permissionsAdded.length ? permissionsAdded.join(', ') : 'already present'}`);
|
|
73
|
+
console.log(`MCP servers detected: ${mcpServers.length ? mcpServers.map((s) => `${s.name} (${s.scope}, ${s.classification})`).join(', ') : 'none'}`);
|
|
74
|
+
|
|
75
|
+
console.log(`\nVerify: ${result.ok ? 'OK' : 'FAILED'}`);
|
|
76
|
+
for (const check of result.checks) {
|
|
77
|
+
console.log(` ${check.ok ? '✓' : '✗'} ${check.name}${check.detail ? ` — ${check.detail}` : ''}`);
|
|
78
|
+
}
|
|
79
|
+
if (!result.ok) process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
84
|
+
usage();
|
|
85
|
+
} else {
|
|
86
|
+
const cwdFlagIdx = args.indexOf('--cwd');
|
|
87
|
+
const cwd = cwdFlagIdx !== -1 ? resolve(args[cwdFlagIdx + 1]) : process.cwd();
|
|
88
|
+
runInit(cwd);
|
|
89
|
+
}
|
package/cli/detect.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Read-only detection: stack, existing config, MCP servers across every
|
|
2
|
+
// scope. Facts over guesses — every value here is read from a real file, none
|
|
3
|
+
// invented. Mirrors skills/tune-context/SKILL.md §1 "Detect".
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
function readJson(path) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Resolve the base Claude config directory, honoring CLAUDE_CONFIG_DIR. */
|
|
17
|
+
export function claudeDirs() {
|
|
18
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
19
|
+
return {
|
|
20
|
+
claudeDir: override || join(homedir(), '.claude'),
|
|
21
|
+
claudeJsonPath: override ? join(override, '.claude.json') : join(homedir(), '.claude.json'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Package manager / language / verified scripts / monorepo shape. Facts only — no guessed commands. */
|
|
26
|
+
export function detectStack(cwd) {
|
|
27
|
+
const pkgPath = join(cwd, 'package.json');
|
|
28
|
+
const pkg = readJson(pkgPath);
|
|
29
|
+
if (pkg) {
|
|
30
|
+
const scripts = pkg.scripts || {};
|
|
31
|
+
const monorepo = Boolean(
|
|
32
|
+
pkg.workspaces || existsSync(join(cwd, 'pnpm-workspace.yaml')) || existsSync(join(cwd, 'turbo.json')) || existsSync(join(cwd, 'nx.json')),
|
|
33
|
+
);
|
|
34
|
+
return {
|
|
35
|
+
ecosystem: 'node',
|
|
36
|
+
name: pkg.name || null,
|
|
37
|
+
packageManager: existsSync(join(cwd, 'pnpm-lock.yaml'))
|
|
38
|
+
? 'pnpm'
|
|
39
|
+
: existsSync(join(cwd, 'yarn.lock'))
|
|
40
|
+
? 'yarn'
|
|
41
|
+
: 'npm',
|
|
42
|
+
buildCmd: scripts.build ? 'npm run build' : null,
|
|
43
|
+
testCmd: scripts.test ? 'npm test' : null,
|
|
44
|
+
lintCmd: scripts.lint ? 'npm run lint' : null,
|
|
45
|
+
monorepo,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
for (const [file, ecosystem] of [
|
|
49
|
+
['pyproject.toml', 'python'],
|
|
50
|
+
['go.mod', 'go'],
|
|
51
|
+
['Cargo.toml', 'rust'],
|
|
52
|
+
]) {
|
|
53
|
+
if (existsSync(join(cwd, file))) {
|
|
54
|
+
return { ecosystem, name: null, packageManager: null, buildCmd: null, testCmd: null, lintCmd: null, monorepo: false, undetectedCommands: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { ecosystem: 'unknown', name: null, packageManager: null, buildCmd: null, testCmd: null, lintCmd: null, monorepo: false, undetectedCommands: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Existing config files this run must merge into, never clobber. */
|
|
61
|
+
export function detectExistingConfig(cwd) {
|
|
62
|
+
const { claudeDir } = claudeDirs();
|
|
63
|
+
return {
|
|
64
|
+
projectClaudeMd: existsSync(join(cwd, 'CLAUDE.md')) ? join(cwd, 'CLAUDE.md') : null,
|
|
65
|
+
userClaudeMd: existsSync(join(claudeDir, 'CLAUDE.md')) ? join(claudeDir, 'CLAUDE.md') : null,
|
|
66
|
+
projectMcpJson: existsSync(join(cwd, '.mcp.json')) ? join(cwd, '.mcp.json') : null,
|
|
67
|
+
projectSettingsJson: join(cwd, '.claude', 'settings.json'),
|
|
68
|
+
userSettingsJson: join(claudeDir, 'settings.json'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Heuristic only — classifies by server name/command, does not introspect tools/list. */
|
|
73
|
+
function classifyMcpServer(name, config) {
|
|
74
|
+
const haystack = [name, config?.command, ...(config?.args || [])].filter(Boolean).join(' ').toLowerCase();
|
|
75
|
+
if (/search|rag|embed|semantic|vector/.test(haystack)) return 'semantic code search';
|
|
76
|
+
if (/wiki|confluence|notion|docs?\b/.test(haystack)) return 'wiki / docs';
|
|
77
|
+
if (/postgres|mysql|sqlite|mongo|\bsql\b|\bdb\b/.test(haystack)) return 'database';
|
|
78
|
+
if (/git(hub|lab)?|issue|jira/.test(haystack)) return 'VCS / issues';
|
|
79
|
+
return 'unknown';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Every MCP server visible to this project, across all three scopes, each
|
|
84
|
+
* tagged with scope + a heuristic classification (name/command keywords —
|
|
85
|
+
* NOT a tools/list introspection; treat as a starting point, not ground truth).
|
|
86
|
+
*/
|
|
87
|
+
export function detectMcpServers(cwd) {
|
|
88
|
+
const { claudeJsonPath } = claudeDirs();
|
|
89
|
+
const servers = [];
|
|
90
|
+
|
|
91
|
+
const projectMcp = readJson(join(cwd, '.mcp.json'));
|
|
92
|
+
for (const [name, config] of Object.entries(projectMcp?.mcpServers || {})) {
|
|
93
|
+
servers.push({ scope: 'project', name, classification: classifyMcpServer(name, config) });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const claudeJson = readJson(claudeJsonPath);
|
|
97
|
+
for (const [name, config] of Object.entries(claudeJson?.mcpServers || {})) {
|
|
98
|
+
servers.push({ scope: 'user', name, classification: classifyMcpServer(name, config) });
|
|
99
|
+
}
|
|
100
|
+
const projectEntry = claudeJson?.[cwd];
|
|
101
|
+
for (const [name, config] of Object.entries(projectEntry?.mcpServers || {})) {
|
|
102
|
+
servers.push({ scope: 'local', name, classification: classifyMcpServer(name, config) });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return servers;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** MCP gating keys from a settings.json — narrows which detected servers are actually reachable. */
|
|
109
|
+
export function detectMcpGating(settingsJsonPath) {
|
|
110
|
+
const settings = readJson(settingsJsonPath);
|
|
111
|
+
if (!settings) return null;
|
|
112
|
+
const { enabledMcpjsonServers, disabledMcpjsonServers, allowedMcpServers, deniedMcpServers } = settings;
|
|
113
|
+
return { enabledMcpjsonServers, disabledMcpjsonServers, allowedMcpServers, deniedMcpServers };
|
|
114
|
+
}
|
package/cli/generate.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Idempotent generation: project CLAUDE.md (managed block), user CLAUDE.md
|
|
2
|
+
// (only if missing), doctrine skills/agents/hooks, and settings.json merges
|
|
3
|
+
// (hooks as a set keyed by command, permissions.allow as a union). Never
|
|
4
|
+
// clobbers hand-written content. Mirrors SKILL.md §2 "Generate".
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { syncDoctrine } from './sync-doctrine.mjs';
|
|
8
|
+
import { upsertManagedBlock } from './managed-block.mjs';
|
|
9
|
+
|
|
10
|
+
function readJson(path, fallback) {
|
|
11
|
+
if (!existsSync(path)) return fallback;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
14
|
+
} catch {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJson(path, obj) {
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, `${JSON.stringify(obj, null, 2)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fillTemplate(template, slots) {
|
|
25
|
+
let out = template;
|
|
26
|
+
for (const [key, value] of Object.entries(slots)) {
|
|
27
|
+
out = out.replaceAll(`{{${key}}}`, value);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract the managed-block inner content from a project CLAUDE.md template file. */
|
|
33
|
+
function templateInnerBlock(templatePath) {
|
|
34
|
+
const raw = readFileSync(templatePath, 'utf8');
|
|
35
|
+
const start = raw.indexOf('<!-- >>> tune-context (managed) >>>');
|
|
36
|
+
const end = raw.indexOf('<!-- <<< tune-context (managed) <<< -->');
|
|
37
|
+
const startLineEnd = raw.indexOf('\n', start) + 1;
|
|
38
|
+
return raw.slice(startLineEnd, end).trimEnd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateProjectClaudeMd(cwd, detection, templatesDir) {
|
|
42
|
+
const templatePath = join(templatesDir, 'CLAUDE.md.project.tmpl');
|
|
43
|
+
const inner = templateInnerBlock(templatePath);
|
|
44
|
+
|
|
45
|
+
const mcpRules = detection.mcpServers.length
|
|
46
|
+
? detection.mcpServers.map((s) => `- \`${s.name}\` (${s.scope}, ${s.classification}) — see token-efficiency skill for the matching rule.`).join('\n')
|
|
47
|
+
: '<!-- fill in: no MCP server detected — none, or add one -->';
|
|
48
|
+
|
|
49
|
+
const filled = fillTemplate(inner, {
|
|
50
|
+
PROJECT_NAME: detection.stack.name || '<!-- fill in: project name -->',
|
|
51
|
+
ONE_LINE_WHAT_THIS_IS: '<!-- fill in: one sentence, what this project is -->',
|
|
52
|
+
BUILD_CMD: detection.stack.buildCmd || '# no build script detected — fill in or delete this line',
|
|
53
|
+
TEST_CMD: detection.stack.testCmd || '# no test script detected — fill in or delete this line',
|
|
54
|
+
LINT_CMD: detection.stack.lintCmd || '# no lint script detected — fill in or delete this line',
|
|
55
|
+
RUN_NOTES: detection.stack.monorepo ? '<!-- monorepo detected — note which package/workspace to run commands from -->' : '',
|
|
56
|
+
MCP_RULES: mcpRules,
|
|
57
|
+
ARCHITECTURE_BULLETS: '<!-- fill in: 3-6 lines, module -> responsibility -->',
|
|
58
|
+
CONVENTIONS: '<!-- fill in: only rules that bite -->',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const path = join(cwd, 'CLAUDE.md');
|
|
62
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
63
|
+
const { content, changed } = upsertManagedBlock(existing, filled);
|
|
64
|
+
if (changed) writeFileSync(path, content);
|
|
65
|
+
return { path, changed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function generateUserClaudeMd(claudeDir, templatesDir) {
|
|
69
|
+
const path = join(claudeDir, 'CLAUDE.md');
|
|
70
|
+
if (existsSync(path)) return { path, changed: false, skipped: 'already exists — never overwrite a user-tuned file' };
|
|
71
|
+
const templatePath = join(templatesDir, 'CLAUDE.md.user.tmpl');
|
|
72
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
73
|
+
writeFileSync(path, readFileSync(templatePath, 'utf8'));
|
|
74
|
+
return { path, changed: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function installDoctrine(pkgRoot, claudeDir) {
|
|
78
|
+
return syncDoctrine(pkgRoot, claudeDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Merge the hooks template into a settings.json, keyed by `command` so a second run is a no-op. */
|
|
82
|
+
export function mergeHooks(settingsPath, hooksTemplate) {
|
|
83
|
+
const settings = readJson(settingsPath, {});
|
|
84
|
+
settings.hooks = settings.hooks || {};
|
|
85
|
+
const added = [];
|
|
86
|
+
for (const [event, groups] of Object.entries(hooksTemplate.hooks)) {
|
|
87
|
+
settings.hooks[event] = settings.hooks[event] || [];
|
|
88
|
+
for (const group of groups) {
|
|
89
|
+
for (const hook of group.hooks) {
|
|
90
|
+
const alreadyPresent = settings.hooks[event].some((g) => g.hooks?.some((h) => h.command === hook.command));
|
|
91
|
+
if (!alreadyPresent) {
|
|
92
|
+
settings.hooks[event].push(group);
|
|
93
|
+
added.push(`${event}: ${hook.command}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (added.length) writeJson(settingsPath, settings);
|
|
99
|
+
return added;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Union permissions.allow with the detected commands — dedupe, never remove entries. */
|
|
103
|
+
export function mergePermissions(settingsPath, allowEntries) {
|
|
104
|
+
const settings = readJson(settingsPath, {});
|
|
105
|
+
settings.permissions = settings.permissions || {};
|
|
106
|
+
const existing = settings.permissions.allow || [];
|
|
107
|
+
const merged = [...new Set([...existing, ...allowEntries])];
|
|
108
|
+
const added = allowEntries.filter((e) => !existing.includes(e));
|
|
109
|
+
if (added.length) {
|
|
110
|
+
settings.permissions.allow = merged;
|
|
111
|
+
writeJson(settingsPath, settings);
|
|
112
|
+
}
|
|
113
|
+
return added;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildPermissionsAllow(detection) {
|
|
117
|
+
const allow = ['Bash(git status:*)', 'Bash(git diff:*)', 'Bash(git log:*)'];
|
|
118
|
+
if (detection.stack.buildCmd) allow.push(`Bash(${detection.stack.buildCmd}:*)`);
|
|
119
|
+
if (detection.stack.testCmd) allow.push(`Bash(${detection.stack.testCmd}:*)`);
|
|
120
|
+
if (detection.stack.lintCmd) allow.push(`Bash(${detection.stack.lintCmd}:*)`);
|
|
121
|
+
for (const server of detection.mcpServers) allow.push(`mcp__${server.name}__*`);
|
|
122
|
+
return allow;
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Idempotent managed-block merge for CLAUDE.md — replace only what's between
|
|
2
|
+
// the markers; hand-written content outside them is never touched. If the
|
|
3
|
+
// markers are absent, append a fresh block instead of rewriting the file.
|
|
4
|
+
export const START_MARKER = '<!-- >>> tune-context (managed) >>>';
|
|
5
|
+
export const END_MARKER = '<!-- <<< tune-context (managed) <<< -->';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string|null} existing - current file content, or null if the file doesn't exist yet
|
|
9
|
+
* @param {string} innerBlock - the freshly generated managed content (without markers)
|
|
10
|
+
* @returns {{content: string, changed: boolean}}
|
|
11
|
+
*/
|
|
12
|
+
export function upsertManagedBlock(existing, innerBlock) {
|
|
13
|
+
const block = `${START_MARKER}\n${innerBlock}\n${END_MARKER}`;
|
|
14
|
+
if (existing == null) {
|
|
15
|
+
return { content: `${block}\n`, changed: true };
|
|
16
|
+
}
|
|
17
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
18
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
19
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
20
|
+
const next = existing.slice(0, startIdx) + block + existing.slice(endIdx + END_MARKER.length);
|
|
21
|
+
return { content: next, changed: next !== existing };
|
|
22
|
+
}
|
|
23
|
+
const sep = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
24
|
+
const next = `${existing}${sep}\n${block}\n`;
|
|
25
|
+
return { content: next, changed: true };
|
|
26
|
+
}
|