@rely-ai/caliber 1.18.8 → 1.19.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/README.md +115 -148
- package/dist/bin.js +372 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,45 +14,24 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
Caliber
|
|
18
|
-
|
|
19
|
-
🔑 **API Key Optional** — use your existing Claude Code or Cursor subscription. Or bring your own key (Anthropic, OpenAI, Vertex AI, any OpenAI-compatible endpoint).
|
|
20
|
-
|
|
21
|
-
🧠 **BYOAI** — Caliber works where you do. All LLM processing runs through your own models — no data is sent to third parties.
|
|
22
|
-
|
|
23
|
-
### Why Caliber?
|
|
24
|
-
|
|
25
|
-
Caliber **generates, audits, and maintains** your agentic development sessions.
|
|
26
|
-
|
|
27
|
-
- 🏗️ **Generates, not just score** — builds your CLAUDE.md, Cursor rules, AGENTS.md, skills, and MCP configs from scratch
|
|
28
|
-
- 🔀 **Multi-agent** — one command sets up Claude Code, Cursor, and Codex together
|
|
29
|
-
- 🌍 **Any codebase** — TypeScript, Python, Go, Rust, Terraform, Java, Ruby — detection is fully LLM-driven, not hardcoded
|
|
30
|
-
- 🧩 **Finds and installs skills** — searches community registries and installs relevant skills for your stack
|
|
31
|
-
- 🔗 **Discovers MCP servers** — auto-detects tools your project uses and installs matching MCP servers
|
|
32
|
-
- 🔄 **Keeps configs fresh** — git hooks and session hooks auto-update your docs as your code changes
|
|
33
|
-
- ↩️ **Fully reversible** — automatic backups, score regression guard, and one-command undo
|
|
34
|
-
|
|
35
|
-
## 🚀 Quick Start
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npx @rely-ai/caliber init
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
That's it. On first run, Caliber walks you through provider setup interactively.
|
|
42
|
-
|
|
43
|
-
Or install globally:
|
|
17
|
+
Caliber analyzes your codebase — languages, frameworks, dependencies, architecture — and generates tailored, high-quality configs for **Claude Code**, **Cursor**, and **OpenAI Codex**. If configs already exist, it scores them, fixes what's stale, and keeps everything in sync as your code evolves.
|
|
44
18
|
|
|
45
19
|
```bash
|
|
46
20
|
npm install -g @rely-ai/caliber
|
|
47
21
|
caliber init
|
|
48
22
|
```
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
No API key required — works with your existing **Claude Code** or **Cursor** subscription. Or bring your own key (Anthropic, OpenAI, Vertex AI, any OpenAI-compatible endpoint).
|
|
25
|
+
|
|
26
|
+
## 💡 What Caliber Does
|
|
27
|
+
|
|
28
|
+
| Without Caliber | With Caliber |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Hand-write CLAUDE.md, Cursor rules, AGENTS.md separately | One command generates all three, tuned to your actual codebase |
|
|
31
|
+
| Configs reference files that no longer exist | Deterministic scoring catches stale references and drift |
|
|
32
|
+
| New team members start with no AI context | `caliber init` gives any contributor a complete setup in seconds |
|
|
33
|
+
| Configs diverge across AI tools | Cross-platform parity — Claude, Cursor, and Codex stay consistent |
|
|
34
|
+
| No idea if your config is actually helping | Score your setup (A–F grade) and see exactly what to improve |
|
|
56
35
|
|
|
57
36
|
## ⚙️ How It Works
|
|
58
37
|
|
|
@@ -62,93 +41,63 @@ caliber init
|
|
|
62
41
|
├─ 1. 🔌 Connect Choose your LLM provider — Claude Code seat, Cursor seat,
|
|
63
42
|
│ or an API key (Anthropic, OpenAI, Vertex AI)
|
|
64
43
|
│
|
|
65
|
-
├─ 2. 🔍 Discover
|
|
66
|
-
│ and existing agent configs
|
|
44
|
+
├─ 2. 🔍 Discover Fingerprint your project: languages, frameworks, dependencies,
|
|
45
|
+
│ file structure, and existing agent configs
|
|
67
46
|
│
|
|
68
|
-
├─ 3. 🛠️ Generate
|
|
69
|
-
│ model
|
|
47
|
+
├─ 3. 🛠️ Generate Build optimized configs with parallel LLM calls
|
|
48
|
+
│ (heavy model for docs, fast model for skills)
|
|
70
49
|
│
|
|
71
|
-
├─ 4. 👀 Review See a diff of proposed
|
|
72
|
-
│ chat, or decline. All
|
|
50
|
+
├─ 4. 👀 Review See a diff of every proposed change — accept, refine
|
|
51
|
+
│ via chat, or decline. All originals are backed up
|
|
73
52
|
│
|
|
74
|
-
└─ 5. 🧩 Skills Search community
|
|
53
|
+
└─ 5. 🧩 Skills Search community registries and install relevant
|
|
75
54
|
skills for your tech stack
|
|
76
55
|
```
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
Already have a setup? If your existing config scores **95+**, Caliber skips full regeneration and applies targeted fixes to the specific checks that are failing.
|
|
79
58
|
|
|
80
59
|
### 📦 What It Generates
|
|
81
60
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| `AGENTS.md` | Codex | Project context for OpenAI Codex |
|
|
88
|
-
| `.claude/skills/*/SKILL.md` | Claude Code | Reusable skill files following [OpenSkills](https://agentskills.io) |
|
|
89
|
-
| `.cursor/skills/*/SKILL.md` | Cursor | Skills for Cursor |
|
|
90
|
-
| `.agents/skills/*/SKILL.md` | Codex | Skills for Codex |
|
|
91
|
-
| `.mcp.json` | Claude Code | MCP server configurations |
|
|
92
|
-
| `.cursor/mcp.json` | Cursor | MCP server configurations |
|
|
93
|
-
| `.claude/settings.json` | Claude Code | Permissions and hooks |
|
|
61
|
+
**Claude Code**
|
|
62
|
+
- `CLAUDE.md` — Project context, build/test commands, architecture, conventions
|
|
63
|
+
- `.claude/skills/*/SKILL.md` — Reusable skills ([OpenSkills](https://agentskills.io) format)
|
|
64
|
+
- `.mcp.json` — Auto-discovered MCP server configurations
|
|
65
|
+
- `.claude/settings.json` — Permissions and hooks
|
|
94
66
|
|
|
95
|
-
|
|
67
|
+
**Cursor**
|
|
68
|
+
- `.cursor/rules/*.mdc` — Modern rules with frontmatter (description, globs, alwaysApply)
|
|
69
|
+
- `.cursor/skills/*/SKILL.md` — Skills for Cursor
|
|
70
|
+
- `.cursor/mcp.json` — MCP server configurations
|
|
96
71
|
|
|
97
|
-
|
|
72
|
+
**OpenAI Codex**
|
|
73
|
+
- `AGENTS.md` — Project context for Codex
|
|
74
|
+
- `.agents/skills/*/SKILL.md` — Skills for Codex
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- 💾 **Automatic backups** — previous versions saved to `.caliber/backups/{timestamp}/` before every write
|
|
102
|
-
- 📊 **Score regression guard** — if a regeneration produces a lower score, changes are auto-reverted
|
|
103
|
-
- ↩️ **Full undo** — `caliber undo` reverts all changes made by Caliber
|
|
104
|
-
- 🔍 **Dry run** — preview any command's changes with `--dry-run`
|
|
105
|
-
|
|
106
|
-
## 📋 Commands
|
|
76
|
+
If these files already exist, Caliber audits them and suggests improvements — keeping what works, fixing what's stale, adding what's missing.
|
|
107
77
|
|
|
108
|
-
|
|
109
|
-
| -------------------- | ---------------------------------------------------- |
|
|
110
|
-
| `caliber init` | 🏁 Initialize your project — full 5-step wizard |
|
|
111
|
-
| `caliber score` | 📊 Score your config quality (deterministic, no LLM) |
|
|
112
|
-
| `caliber skills` | 🧩 Discover and install community skills |
|
|
113
|
-
| `caliber regenerate` | 🔄 Re-analyze and regenerate your setup |
|
|
114
|
-
| `caliber refresh` | 🔃 Update docs based on recent code changes |
|
|
115
|
-
| `caliber hooks` | 🪝 Manage auto-refresh hooks |
|
|
116
|
-
| `caliber config` | ⚙️ Configure LLM provider, API key, and model |
|
|
117
|
-
| `caliber status` | 📌 Show current setup status |
|
|
118
|
-
| `caliber undo` | ↩️ Revert all changes made by Caliber |
|
|
78
|
+
## ✨ Key Features
|
|
119
79
|
|
|
120
|
-
###
|
|
80
|
+
### 🌍 Any Codebase
|
|
81
|
+
TypeScript, Python, Go, Rust, Java, Ruby, Terraform, and more. Language and framework detection is fully LLM-driven — no hardcoded mappings. Caliber works on any project.
|
|
121
82
|
|
|
83
|
+
### 🔀 Any AI Tool
|
|
84
|
+
Target a single platform or all three at once:
|
|
122
85
|
```bash
|
|
123
|
-
#
|
|
124
|
-
caliber init
|
|
125
|
-
caliber init --agent
|
|
126
|
-
caliber init --agent
|
|
127
|
-
caliber init --agent codex # Target OpenAI Codex only
|
|
128
|
-
caliber init --agent all # Target all three
|
|
86
|
+
caliber init --agent claude # Claude Code only
|
|
87
|
+
caliber init --agent cursor # Cursor only
|
|
88
|
+
caliber init --agent codex # Codex only
|
|
89
|
+
caliber init --agent all # All three
|
|
129
90
|
caliber init --agent claude,cursor # Comma-separated
|
|
130
|
-
caliber init --dry-run # Preview without writing files
|
|
131
|
-
caliber init --force # Overwrite existing setup without prompting
|
|
132
|
-
|
|
133
|
-
# Scoring
|
|
134
|
-
caliber score # Full breakdown with grade (A-F)
|
|
135
|
-
caliber score --json # Machine-readable output
|
|
136
|
-
caliber score --agent claude # Score for a specific agent
|
|
137
|
-
|
|
138
|
-
# Day-to-day
|
|
139
|
-
caliber regenerate # Re-analyze and regenerate (alias: regen, re)
|
|
140
|
-
caliber refresh # Update docs from recent git changes
|
|
141
|
-
caliber refresh --dry-run # Preview what would change
|
|
142
|
-
caliber skills # Browse and install community skills
|
|
143
|
-
caliber hooks # Toggle auto-refresh hooks
|
|
144
|
-
caliber hooks --install # Enable all hooks non-interactively
|
|
145
|
-
caliber status # Show what Caliber has set up
|
|
146
|
-
caliber undo # Revert everything
|
|
147
91
|
```
|
|
148
92
|
|
|
149
|
-
|
|
93
|
+
### 💬 Chat-Based Refinement
|
|
94
|
+
Not happy with the generated output? During review, refine via natural language — describe what you want changed and Caliber iterates until you're satisfied.
|
|
150
95
|
|
|
151
|
-
|
|
96
|
+
### 🔗 MCP Server Discovery
|
|
97
|
+
Caliber detects the tools your project uses (databases, APIs, services) and auto-configures matching MCP servers for Claude Code and Cursor.
|
|
98
|
+
|
|
99
|
+
### 📊 Deterministic Scoring
|
|
100
|
+
`caliber score` evaluates your config quality without any LLM calls — purely by cross-referencing config files against your actual project filesystem.
|
|
152
101
|
|
|
153
102
|
```
|
|
154
103
|
Agent Config Score 88 / 100 Grade A
|
|
@@ -161,59 +110,76 @@ caliber undo # Revert everything
|
|
|
161
110
|
BONUS 5 / 5
|
|
162
111
|
```
|
|
163
112
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| **Files & Setup** | 25 | Config files exist, skills present, MCP servers, cross-platform parity |
|
|
167
|
-
| **Quality** | 25 | Has code blocks, concise token budget, concrete instructions, structured headings |
|
|
168
|
-
| **Grounding** | 20 | Config references actual project directories and files |
|
|
169
|
-
| **Accuracy** | 15 | Referenced paths exist on disk, config is in sync with code (git-based) |
|
|
170
|
-
| **Freshness & Safety** | 10 | Recently updated, no leaked secrets, permissions configured |
|
|
171
|
-
| **Bonus** | 5 | Auto-refresh hooks, AGENTS.md, OpenSkills format |
|
|
172
|
-
|
|
173
|
-
Every failing check includes structured fix data — when `caliber init` runs, the LLM receives exactly what's wrong and how to fix it.
|
|
174
|
-
|
|
175
|
-
## 🧩 Skills
|
|
176
|
-
|
|
177
|
-
Caliber searches three community registries and scores results against your project
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
caliber skills
|
|
181
|
-
```
|
|
113
|
+
<details>
|
|
114
|
+
<summary>Scoring breakdown</summary>
|
|
182
115
|
|
|
183
|
-
|
|
116
|
+
| Category | Points | What it checks |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| **Files & Setup** | 25 | Config files exist, skills present, MCP servers, cross-platform parity |
|
|
119
|
+
| **Quality** | 25 | Code blocks, concise token budget, concrete instructions, structured headings |
|
|
120
|
+
| **Grounding** | 20 | Config references actual project directories and files |
|
|
121
|
+
| **Accuracy** | 15 | Referenced paths exist on disk, config freshness vs. git history |
|
|
122
|
+
| **Freshness & Safety** | 10 | Recently updated, no leaked secrets, permissions configured |
|
|
123
|
+
| **Bonus** | 5 | Auto-refresh hooks, AGENTS.md, OpenSkills format |
|
|
184
124
|
|
|
185
|
-
|
|
125
|
+
Every failing check includes structured fix data — when `caliber init` runs, the LLM receives exactly what's wrong and how to fix it.
|
|
186
126
|
|
|
187
|
-
|
|
127
|
+
</details>
|
|
188
128
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
| 🤖 **Claude Code** | End of each session | Runs `caliber refresh` and updates docs |
|
|
192
|
-
| 📝 **Git pre-commit** | Before each commit | Refreshes docs and stages updated files |
|
|
129
|
+
### 🔄 Auto-Refresh
|
|
130
|
+
Keep configs in sync with your codebase automatically:
|
|
193
131
|
|
|
194
|
-
|
|
132
|
+
| Hook | Trigger | What it does |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| **Claude Code** | End of each session | Runs `caliber refresh` and updates docs |
|
|
135
|
+
| **Git pre-commit** | Before each commit | Refreshes docs and stages updated files |
|
|
195
136
|
|
|
196
137
|
```bash
|
|
197
138
|
caliber hooks --install # Enable all hooks
|
|
198
139
|
caliber hooks --remove # Disable all hooks
|
|
199
140
|
```
|
|
200
141
|
|
|
201
|
-
The refresh command analyzes your git diff (committed, staged, and unstaged changes) and updates
|
|
142
|
+
The `refresh` command analyzes your git diff (committed, staged, and unstaged changes) and updates config files to reflect what changed. Works across multiple repos when run from a parent directory.
|
|
143
|
+
|
|
144
|
+
### 🛡️ Fully Reversible
|
|
145
|
+
Every change Caliber makes can be undone:
|
|
146
|
+
- **Automatic backups** — originals saved to `.caliber/backups/` before every write
|
|
147
|
+
- **Score regression guard** — if a regeneration produces a lower score, changes are auto-reverted
|
|
148
|
+
- **Full undo** — `caliber undo` restores everything to its previous state
|
|
149
|
+
- **Dry run** — preview changes with `--dry-run` before applying
|
|
150
|
+
|
|
151
|
+
## 📋 Commands
|
|
152
|
+
|
|
153
|
+
| Command | Description |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `caliber init` | Full setup wizard — analyze, generate, review, install skills |
|
|
156
|
+
| `caliber score` | Score config quality (deterministic, no LLM) |
|
|
157
|
+
| `caliber regenerate` | Re-analyze and regenerate configs (aliases: `regen`, `re`) |
|
|
158
|
+
| `caliber refresh` | Update docs based on recent code changes |
|
|
159
|
+
| `caliber skills` | Discover and install community skills |
|
|
160
|
+
| `caliber hooks` | Manage auto-refresh hooks |
|
|
161
|
+
| `caliber config` | Configure LLM provider, API key, and model |
|
|
162
|
+
| `caliber status` | Show current setup status |
|
|
163
|
+
| `caliber undo` | Revert all changes made by Caliber |
|
|
202
164
|
|
|
203
165
|
## 🔌 LLM Providers
|
|
204
166
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
|
208
|
-
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
167
|
+
No API key? No problem. Caliber works with your existing AI tool subscription:
|
|
168
|
+
|
|
169
|
+
| Provider | Setup | Default Model |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| **Claude Code** (your seat) | `caliber config` → Claude Code | Inherited from Claude Code |
|
|
172
|
+
| **Cursor** (your seat) | `caliber config` → Cursor | Inherited from Cursor |
|
|
173
|
+
| **Anthropic** | `export ANTHROPIC_API_KEY=sk-ant-...` | `claude-sonnet-4-6` |
|
|
174
|
+
| **OpenAI** | `export OPENAI_API_KEY=sk-...` | `gpt-4.1` |
|
|
175
|
+
| **Vertex AI** | `export VERTEX_PROJECT_ID=my-project` | `claude-sonnet-4-6` |
|
|
176
|
+
| **Custom endpoint** | `OPENAI_API_KEY` + `OPENAI_BASE_URL` | `gpt-4.1` |
|
|
213
177
|
|
|
214
178
|
Override the model for any provider: `export CALIBER_MODEL=<model-name>` or use `caliber config`.
|
|
215
179
|
|
|
216
|
-
|
|
180
|
+
Caliber uses a **two-tier model system** — lightweight tasks (classification, scoring) auto-use a faster model, while heavy tasks (generation, refinement) use the default. This keeps costs low and speed high.
|
|
181
|
+
|
|
182
|
+
Configuration is stored in `~/.caliber/config.json` with restricted permissions (`0600`). API keys are never written to project files.
|
|
217
183
|
|
|
218
184
|
<details>
|
|
219
185
|
<summary>Vertex AI advanced setup</summary>
|
|
@@ -237,18 +203,19 @@ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
|
|
|
237
203
|
<details>
|
|
238
204
|
<summary>Environment variables reference</summary>
|
|
239
205
|
|
|
240
|
-
| Variable
|
|
241
|
-
|
|
242
|
-
| `ANTHROPIC_API_KEY`
|
|
243
|
-
| `OPENAI_API_KEY`
|
|
244
|
-
| `OPENAI_BASE_URL`
|
|
245
|
-
| `VERTEX_PROJECT_ID`
|
|
246
|
-
| `VERTEX_REGION`
|
|
247
|
-
| `VERTEX_SA_CREDENTIALS`
|
|
248
|
-
| `GOOGLE_APPLICATION_CREDENTIALS` | Service account JSON file path
|
|
249
|
-
| `CALIBER_USE_CLAUDE_CLI`
|
|
250
|
-
| `CALIBER_USE_CURSOR_SEAT`
|
|
251
|
-
| `CALIBER_MODEL`
|
|
206
|
+
| Variable | Purpose |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
209
|
+
| `OPENAI_API_KEY` | OpenAI API key |
|
|
210
|
+
| `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint |
|
|
211
|
+
| `VERTEX_PROJECT_ID` | GCP project ID for Vertex AI |
|
|
212
|
+
| `VERTEX_REGION` | Vertex AI region (default: `us-east5`) |
|
|
213
|
+
| `VERTEX_SA_CREDENTIALS` | Service account JSON (inline) |
|
|
214
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | Service account JSON file path |
|
|
215
|
+
| `CALIBER_USE_CLAUDE_CLI` | Use Claude Code CLI (`1` to enable) |
|
|
216
|
+
| `CALIBER_USE_CURSOR_SEAT` | Use Cursor subscription (`1` to enable) |
|
|
217
|
+
| `CALIBER_MODEL` | Override model for any provider |
|
|
218
|
+
| `CALIBER_FAST_MODEL` | Override fast model for any provider |
|
|
252
219
|
|
|
253
220
|
</details>
|
|
254
221
|
|
package/dist/bin.js
CHANGED
|
@@ -177,13 +177,13 @@ __export(lock_exports, {
|
|
|
177
177
|
isCaliberRunning: () => isCaliberRunning,
|
|
178
178
|
releaseLock: () => releaseLock
|
|
179
179
|
});
|
|
180
|
-
import
|
|
181
|
-
import
|
|
180
|
+
import fs28 from "fs";
|
|
181
|
+
import path22 from "path";
|
|
182
182
|
import os4 from "os";
|
|
183
183
|
function isCaliberRunning() {
|
|
184
184
|
try {
|
|
185
|
-
if (!
|
|
186
|
-
const raw =
|
|
185
|
+
if (!fs28.existsSync(LOCK_FILE)) return false;
|
|
186
|
+
const raw = fs28.readFileSync(LOCK_FILE, "utf-8").trim();
|
|
187
187
|
const { pid, ts } = JSON.parse(raw);
|
|
188
188
|
if (Date.now() - ts > STALE_MS) return false;
|
|
189
189
|
try {
|
|
@@ -198,13 +198,13 @@ function isCaliberRunning() {
|
|
|
198
198
|
}
|
|
199
199
|
function acquireLock() {
|
|
200
200
|
try {
|
|
201
|
-
|
|
201
|
+
fs28.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
202
202
|
} catch {
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
function releaseLock() {
|
|
206
206
|
try {
|
|
207
|
-
if (
|
|
207
|
+
if (fs28.existsSync(LOCK_FILE)) fs28.unlinkSync(LOCK_FILE);
|
|
208
208
|
} catch {
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -212,14 +212,14 @@ var LOCK_FILE, STALE_MS;
|
|
|
212
212
|
var init_lock = __esm({
|
|
213
213
|
"src/lib/lock.ts"() {
|
|
214
214
|
"use strict";
|
|
215
|
-
LOCK_FILE =
|
|
215
|
+
LOCK_FILE = path22.join(os4.tmpdir(), ".caliber.lock");
|
|
216
216
|
STALE_MS = 10 * 60 * 1e3;
|
|
217
217
|
}
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
// src/cli.ts
|
|
221
221
|
import { Command } from "commander";
|
|
222
|
-
import
|
|
222
|
+
import fs32 from "fs";
|
|
223
223
|
import path25 from "path";
|
|
224
224
|
import { fileURLToPath } from "url";
|
|
225
225
|
|
|
@@ -2030,6 +2030,8 @@ Rules:
|
|
|
2030
2030
|
- Be conservative \u2014 don't rewrite sections that aren't affected by the changes
|
|
2031
2031
|
- Don't add speculative or aspirational content
|
|
2032
2032
|
- Keep managed blocks (<!-- caliber:managed --> ... <!-- /caliber:managed -->) intact
|
|
2033
|
+
- Do NOT modify CALIBER_LEARNINGS.md \u2014 it is managed separately by the learning system
|
|
2034
|
+
- Preserve any references to CALIBER_LEARNINGS.md in CLAUDE.md
|
|
2033
2035
|
- If a doc doesn't need updating, return null for it
|
|
2034
2036
|
- For CLAUDE.md: update commands, architecture notes, conventions, key files if the diffs affect them. Keep under 150 lines.
|
|
2035
2037
|
- For README.md: update setup instructions, API docs, or feature descriptions if affected
|
|
@@ -2061,20 +2063,25 @@ Your job is to reason deeply about these events and identify:
|
|
|
2061
2063
|
3. **Workarounds**: When the agent had to abandon one approach entirely and use a different strategy
|
|
2062
2064
|
4. **Repeated struggles**: The same tool being called many times against the same target, indicating confusion or trial-and-error
|
|
2063
2065
|
5. **Project-specific conventions**: Commands, paths, patterns, or configurations that are specific to this project and would help future sessions
|
|
2066
|
+
6. **Anti-patterns**: Commands, approaches, or configurations that consistently fail or cause problems \u2014 things future sessions should explicitly avoid
|
|
2064
2067
|
|
|
2065
2068
|
From these observations, produce:
|
|
2066
2069
|
|
|
2067
2070
|
### claudeMdLearnedSection
|
|
2068
|
-
A markdown section with concise, actionable bullet points
|
|
2071
|
+
A markdown section with concise, actionable bullet points. Your output will be written to CALIBER_LEARNINGS.md \u2014 a standalone file that all AI coding agents (Claude Code, Cursor, Codex) reference for project-specific patterns and anti-patterns. Each bullet should be a concrete instruction that prevents a past mistake or encodes a discovered convention. Examples:
|
|
2069
2072
|
- "Always run \`npm install\` before \`npm run build\` in this project"
|
|
2070
2073
|
- "The test database requires \`DATABASE_URL\` to be set \u2014 use \`source .env.test\` first"
|
|
2071
2074
|
- "TypeScript strict mode is enabled \u2014 never use \`any\`, use \`unknown\` with type guards"
|
|
2072
2075
|
- "Use \`pnpm\` not \`npm\` \u2014 the lockfile is pnpm-lock.yaml"
|
|
2076
|
+
- "Never use \`npm\` in this project \u2014 pnpm-lock.yaml is the lockfile"
|
|
2077
|
+
- "Do NOT run \`jest\` directly \u2014 always use \`npm run test\` which sets the correct env"
|
|
2078
|
+
- "Avoid modifying files in \`src/generated/\` \u2014 they are auto-generated by the build step"
|
|
2073
2079
|
|
|
2074
2080
|
Rules for the learned section:
|
|
2075
2081
|
- Be additive: keep all existing learned items, add new ones, remove duplicates
|
|
2076
2082
|
- Never repeat instructions already present in the main CLAUDE.md
|
|
2077
2083
|
- Each bullet must be specific and actionable \u2014 no vague advice
|
|
2084
|
+
- Include both positive directives ('Always do X') and negative rules ('Never do Y because Z') when the session evidence supports them
|
|
2078
2085
|
- Maximum ~30 bullet items total
|
|
2079
2086
|
- Group related items under subheadings if there are many
|
|
2080
2087
|
- If there's nothing meaningful to learn, return null
|
|
@@ -3593,7 +3600,8 @@ function getPrecommitBlock() {
|
|
|
3593
3600
|
if [ -x "${bin}" ] || command -v "${bin}" >/dev/null 2>&1; then
|
|
3594
3601
|
echo "\\033[2mcaliber: refreshing docs...\\033[0m"
|
|
3595
3602
|
"${bin}" refresh 2>/dev/null || true
|
|
3596
|
-
|
|
3603
|
+
"${bin}" learn finalize 2>/dev/null || true
|
|
3604
|
+
git diff --name-only -- CLAUDE.md .claude/ .cursor/ AGENTS.md CALIBER_LEARNINGS.md 2>/dev/null | xargs git add 2>/dev/null || true
|
|
3597
3605
|
fi
|
|
3598
3606
|
${PRECOMMIT_END}`;
|
|
3599
3607
|
}
|
|
@@ -3717,6 +3725,69 @@ function installLearningHooks() {
|
|
|
3717
3725
|
writeSettings2(settings);
|
|
3718
3726
|
return { installed: true, alreadyInstalled: false };
|
|
3719
3727
|
}
|
|
3728
|
+
var CURSOR_HOOKS_PATH = path13.join(".cursor", "hooks.json");
|
|
3729
|
+
var CURSOR_HOOK_EVENTS = [
|
|
3730
|
+
{ event: "postToolUse", tail: "learn observe" },
|
|
3731
|
+
{ event: "postToolUseFailure", tail: "learn observe --failure" },
|
|
3732
|
+
{ event: "sessionEnd", tail: "learn finalize" }
|
|
3733
|
+
];
|
|
3734
|
+
function readCursorHooks() {
|
|
3735
|
+
if (!fs18.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
|
|
3736
|
+
try {
|
|
3737
|
+
return JSON.parse(fs18.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
|
|
3738
|
+
} catch {
|
|
3739
|
+
return { version: 1, hooks: {} };
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
function writeCursorHooks(config) {
|
|
3743
|
+
const dir = path13.dirname(CURSOR_HOOKS_PATH);
|
|
3744
|
+
if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
|
|
3745
|
+
fs18.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
|
|
3746
|
+
}
|
|
3747
|
+
function hasCursorHook(entries, tail) {
|
|
3748
|
+
return entries.some((e) => isCaliberCommand(e.command, tail));
|
|
3749
|
+
}
|
|
3750
|
+
function areCursorLearningHooksInstalled() {
|
|
3751
|
+
const config = readCursorHooks();
|
|
3752
|
+
return CURSOR_HOOK_EVENTS.every((cfg) => {
|
|
3753
|
+
const entries = config.hooks[cfg.event];
|
|
3754
|
+
return Array.isArray(entries) && hasCursorHook(entries, cfg.tail);
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
function installCursorLearningHooks() {
|
|
3758
|
+
if (areCursorLearningHooksInstalled()) {
|
|
3759
|
+
return { installed: false, alreadyInstalled: true };
|
|
3760
|
+
}
|
|
3761
|
+
const config = readCursorHooks();
|
|
3762
|
+
const bin = resolveCaliber();
|
|
3763
|
+
for (const cfg of CURSOR_HOOK_EVENTS) {
|
|
3764
|
+
if (!Array.isArray(config.hooks[cfg.event])) {
|
|
3765
|
+
config.hooks[cfg.event] = [];
|
|
3766
|
+
}
|
|
3767
|
+
if (!hasCursorHook(config.hooks[cfg.event], cfg.tail)) {
|
|
3768
|
+
config.hooks[cfg.event].push({ command: `${bin} ${cfg.tail}` });
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
writeCursorHooks(config);
|
|
3772
|
+
return { installed: true, alreadyInstalled: false };
|
|
3773
|
+
}
|
|
3774
|
+
function removeCursorLearningHooks() {
|
|
3775
|
+
const config = readCursorHooks();
|
|
3776
|
+
let removedAny = false;
|
|
3777
|
+
for (const cfg of CURSOR_HOOK_EVENTS) {
|
|
3778
|
+
const entries = config.hooks[cfg.event];
|
|
3779
|
+
if (!Array.isArray(entries)) continue;
|
|
3780
|
+
const idx = entries.findIndex((e) => isCaliberCommand(e.command, cfg.tail));
|
|
3781
|
+
if (idx !== -1) {
|
|
3782
|
+
entries.splice(idx, 1);
|
|
3783
|
+
removedAny = true;
|
|
3784
|
+
if (entries.length === 0) delete config.hooks[cfg.event];
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
if (!removedAny) return { removed: false, notFound: true };
|
|
3788
|
+
writeCursorHooks(config);
|
|
3789
|
+
return { removed: true, notFound: false };
|
|
3790
|
+
}
|
|
3720
3791
|
function removeLearningHooks() {
|
|
3721
3792
|
const settings = readSettings2();
|
|
3722
3793
|
if (!settings.hooks) return { removed: false, notFound: true };
|
|
@@ -3998,6 +4069,7 @@ var POINTS_PERMISSIONS = 2;
|
|
|
3998
4069
|
var POINTS_HOOKS = 2;
|
|
3999
4070
|
var POINTS_AGENTS_MD = 1;
|
|
4000
4071
|
var POINTS_OPEN_SKILLS_FORMAT = 2;
|
|
4072
|
+
var POINTS_LEARNED_CONTENT = 2;
|
|
4001
4073
|
var TOKEN_BUDGET_THRESHOLDS = [
|
|
4002
4074
|
{ maxTokens: 2e3, points: 6 },
|
|
4003
4075
|
{ maxTokens: 3500, points: 5 },
|
|
@@ -5106,6 +5178,18 @@ function checkBonus(dir) {
|
|
|
5106
5178
|
instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
|
|
5107
5179
|
} : void 0
|
|
5108
5180
|
});
|
|
5181
|
+
const learningsContent = readFileOrNull2(join7(dir, "CALIBER_LEARNINGS.md"));
|
|
5182
|
+
const hasLearned = learningsContent ? learningsContent.split("\n").filter((l) => l.startsWith("- ")).length > 0 : false;
|
|
5183
|
+
checks.push({
|
|
5184
|
+
id: "learned_content",
|
|
5185
|
+
name: "Learned content present",
|
|
5186
|
+
category: "bonus",
|
|
5187
|
+
maxPoints: POINTS_LEARNED_CONTENT,
|
|
5188
|
+
earnedPoints: hasLearned ? POINTS_LEARNED_CONTENT : 0,
|
|
5189
|
+
passed: hasLearned,
|
|
5190
|
+
detail: hasLearned ? "Session learnings found in CALIBER_LEARNINGS.md" : "No learned content",
|
|
5191
|
+
suggestion: hasLearned ? void 0 : "Install learning hooks: `caliber learn install`"
|
|
5192
|
+
});
|
|
5109
5193
|
return checks;
|
|
5110
5194
|
}
|
|
5111
5195
|
|
|
@@ -7314,8 +7398,8 @@ async function scoreCommand(options) {
|
|
|
7314
7398
|
}
|
|
7315
7399
|
|
|
7316
7400
|
// src/commands/refresh.ts
|
|
7317
|
-
import
|
|
7318
|
-
import
|
|
7401
|
+
import fs29 from "fs";
|
|
7402
|
+
import path23 from "path";
|
|
7319
7403
|
import chalk14 from "chalk";
|
|
7320
7404
|
import ora5 from "ora";
|
|
7321
7405
|
|
|
@@ -7327,7 +7411,8 @@ var DOC_PATTERNS = [
|
|
|
7327
7411
|
"README.md",
|
|
7328
7412
|
".cursorrules",
|
|
7329
7413
|
".cursor/rules/",
|
|
7330
|
-
".claude/skills/"
|
|
7414
|
+
".claude/skills/",
|
|
7415
|
+
"CALIBER_LEARNINGS.md"
|
|
7331
7416
|
];
|
|
7332
7417
|
function excludeArgs() {
|
|
7333
7418
|
return DOC_PATTERNS.flatMap((p) => ["--", `:!${p}`]);
|
|
@@ -7431,8 +7516,8 @@ function writeRefreshDocs(docs) {
|
|
|
7431
7516
|
|
|
7432
7517
|
// src/ai/refresh.ts
|
|
7433
7518
|
init_config();
|
|
7434
|
-
async function refreshDocs(diff, existingDocs, projectContext) {
|
|
7435
|
-
const prompt = buildRefreshPrompt(diff, existingDocs, projectContext);
|
|
7519
|
+
async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
|
|
7520
|
+
const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection);
|
|
7436
7521
|
const fastModel = getFastModel();
|
|
7437
7522
|
const raw = await llmCall({
|
|
7438
7523
|
system: REFRESH_SYSTEM_PROMPT,
|
|
@@ -7442,7 +7527,7 @@ async function refreshDocs(diff, existingDocs, projectContext) {
|
|
|
7442
7527
|
});
|
|
7443
7528
|
return parseJsonResponse(raw);
|
|
7444
7529
|
}
|
|
7445
|
-
function buildRefreshPrompt(diff, existingDocs, projectContext) {
|
|
7530
|
+
function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection) {
|
|
7446
7531
|
const parts = [];
|
|
7447
7532
|
parts.push("Update documentation based on the following code changes.\n");
|
|
7448
7533
|
if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
|
|
@@ -7490,9 +7575,144 @@ Changed files: ${diff.changedFiles.join(", ")}`);
|
|
|
7490
7575
|
parts.push(rule.content);
|
|
7491
7576
|
}
|
|
7492
7577
|
}
|
|
7578
|
+
if (learnedSection) {
|
|
7579
|
+
parts.push("\n--- Learned Patterns (from session learning) ---");
|
|
7580
|
+
parts.push("Consider these accumulated learnings when deciding what to update:");
|
|
7581
|
+
parts.push(learnedSection);
|
|
7582
|
+
}
|
|
7493
7583
|
return parts.join("\n");
|
|
7494
7584
|
}
|
|
7495
7585
|
|
|
7586
|
+
// src/learner/writer.ts
|
|
7587
|
+
import fs27 from "fs";
|
|
7588
|
+
import path21 from "path";
|
|
7589
|
+
var LEARNINGS_FILE = "CALIBER_LEARNINGS.md";
|
|
7590
|
+
var LEARNINGS_HEADER = `# Caliber Learnings
|
|
7591
|
+
|
|
7592
|
+
Accumulated patterns and anti-patterns from development sessions.
|
|
7593
|
+
Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
|
|
7594
|
+
|
|
7595
|
+
`;
|
|
7596
|
+
var LEARNED_START = "<!-- caliber:learned -->";
|
|
7597
|
+
var LEARNED_END = "<!-- /caliber:learned -->";
|
|
7598
|
+
var MAX_LEARNED_ITEMS = 30;
|
|
7599
|
+
function writeLearnedContent(update) {
|
|
7600
|
+
const written = [];
|
|
7601
|
+
let newItemCount = 0;
|
|
7602
|
+
let newItems = [];
|
|
7603
|
+
if (update.claudeMdLearnedSection) {
|
|
7604
|
+
const result = writeLearnedSection(update.claudeMdLearnedSection);
|
|
7605
|
+
newItemCount = result.newCount;
|
|
7606
|
+
newItems = result.newItems;
|
|
7607
|
+
written.push(LEARNINGS_FILE);
|
|
7608
|
+
}
|
|
7609
|
+
if (update.skills?.length) {
|
|
7610
|
+
for (const skill of update.skills) {
|
|
7611
|
+
const skillPath = writeLearnedSkill(skill);
|
|
7612
|
+
written.push(skillPath);
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
return { written, newItemCount, newItems };
|
|
7616
|
+
}
|
|
7617
|
+
function parseBullets(content) {
|
|
7618
|
+
const lines = content.split("\n");
|
|
7619
|
+
const bullets = [];
|
|
7620
|
+
let current = "";
|
|
7621
|
+
for (const line of lines) {
|
|
7622
|
+
if (line.startsWith("- ")) {
|
|
7623
|
+
if (current) bullets.push(current);
|
|
7624
|
+
current = line;
|
|
7625
|
+
} else if (current && line.trim() && !line.startsWith("#")) {
|
|
7626
|
+
current += "\n" + line;
|
|
7627
|
+
} else {
|
|
7628
|
+
if (current) bullets.push(current);
|
|
7629
|
+
current = "";
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
if (current) bullets.push(current);
|
|
7633
|
+
return bullets;
|
|
7634
|
+
}
|
|
7635
|
+
function normalizeBullet(bullet) {
|
|
7636
|
+
return bullet.replace(/^- /, "").replace(/`[^`]*`/g, "").replace(/\s+/g, " ").toLowerCase().trim();
|
|
7637
|
+
}
|
|
7638
|
+
function deduplicateLearnedItems(existing, incoming) {
|
|
7639
|
+
const existingBullets = existing ? parseBullets(existing) : [];
|
|
7640
|
+
const incomingBullets = parseBullets(incoming);
|
|
7641
|
+
const merged = [...existingBullets];
|
|
7642
|
+
const newItems = [];
|
|
7643
|
+
for (const bullet of incomingBullets) {
|
|
7644
|
+
const norm = normalizeBullet(bullet);
|
|
7645
|
+
if (!norm) continue;
|
|
7646
|
+
const isDup = merged.some((e) => {
|
|
7647
|
+
const eNorm = normalizeBullet(e);
|
|
7648
|
+
const shorter = Math.min(norm.length, eNorm.length);
|
|
7649
|
+
const longer = Math.max(norm.length, eNorm.length);
|
|
7650
|
+
if (!(eNorm.includes(norm) || norm.includes(eNorm))) return false;
|
|
7651
|
+
return shorter / longer > 0.7;
|
|
7652
|
+
});
|
|
7653
|
+
if (!isDup) {
|
|
7654
|
+
merged.push(bullet);
|
|
7655
|
+
newItems.push(bullet);
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
const capped = merged.length > MAX_LEARNED_ITEMS ? merged.slice(-MAX_LEARNED_ITEMS) : merged;
|
|
7659
|
+
return { merged: capped.join("\n"), newCount: newItems.length, newItems };
|
|
7660
|
+
}
|
|
7661
|
+
function writeLearnedSection(content) {
|
|
7662
|
+
const existingSection = readLearnedSection();
|
|
7663
|
+
const { merged, newCount, newItems } = deduplicateLearnedItems(existingSection, content);
|
|
7664
|
+
fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + merged + "\n");
|
|
7665
|
+
return { newCount, newItems };
|
|
7666
|
+
}
|
|
7667
|
+
function writeLearnedSkill(skill) {
|
|
7668
|
+
const skillDir = path21.join(".claude", "skills", skill.name);
|
|
7669
|
+
if (!fs27.existsSync(skillDir)) fs27.mkdirSync(skillDir, { recursive: true });
|
|
7670
|
+
const skillPath = path21.join(skillDir, "SKILL.md");
|
|
7671
|
+
if (!skill.isNew && fs27.existsSync(skillPath)) {
|
|
7672
|
+
const existing = fs27.readFileSync(skillPath, "utf-8");
|
|
7673
|
+
fs27.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
|
|
7674
|
+
} else {
|
|
7675
|
+
const frontmatter = [
|
|
7676
|
+
"---",
|
|
7677
|
+
`name: ${skill.name}`,
|
|
7678
|
+
`description: ${skill.description}`,
|
|
7679
|
+
"---",
|
|
7680
|
+
""
|
|
7681
|
+
].join("\n");
|
|
7682
|
+
fs27.writeFileSync(skillPath, frontmatter + skill.content);
|
|
7683
|
+
}
|
|
7684
|
+
return skillPath;
|
|
7685
|
+
}
|
|
7686
|
+
function readLearnedSection() {
|
|
7687
|
+
if (fs27.existsSync(LEARNINGS_FILE)) {
|
|
7688
|
+
const content2 = fs27.readFileSync(LEARNINGS_FILE, "utf-8");
|
|
7689
|
+
const bullets = content2.split("\n").filter((l) => l.startsWith("- ")).join("\n");
|
|
7690
|
+
return bullets || null;
|
|
7691
|
+
}
|
|
7692
|
+
const claudeMdPath = "CLAUDE.md";
|
|
7693
|
+
if (!fs27.existsSync(claudeMdPath)) return null;
|
|
7694
|
+
const content = fs27.readFileSync(claudeMdPath, "utf-8");
|
|
7695
|
+
const startIdx = content.indexOf(LEARNED_START);
|
|
7696
|
+
const endIdx = content.indexOf(LEARNED_END);
|
|
7697
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
7698
|
+
return content.slice(startIdx + LEARNED_START.length, endIdx).trim() || null;
|
|
7699
|
+
}
|
|
7700
|
+
function migrateInlineLearnings() {
|
|
7701
|
+
if (fs27.existsSync(LEARNINGS_FILE)) return false;
|
|
7702
|
+
const claudeMdPath = "CLAUDE.md";
|
|
7703
|
+
if (!fs27.existsSync(claudeMdPath)) return false;
|
|
7704
|
+
const content = fs27.readFileSync(claudeMdPath, "utf-8");
|
|
7705
|
+
const startIdx = content.indexOf(LEARNED_START);
|
|
7706
|
+
const endIdx = content.indexOf(LEARNED_END);
|
|
7707
|
+
if (startIdx === -1 || endIdx === -1) return false;
|
|
7708
|
+
const section = content.slice(startIdx + LEARNED_START.length, endIdx).trim();
|
|
7709
|
+
if (!section) return false;
|
|
7710
|
+
fs27.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
|
|
7711
|
+
const cleaned = content.slice(0, startIdx) + content.slice(endIdx + LEARNED_END.length);
|
|
7712
|
+
fs27.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
|
|
7713
|
+
return true;
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7496
7716
|
// src/commands/refresh.ts
|
|
7497
7717
|
init_config();
|
|
7498
7718
|
function log2(quiet, ...args) {
|
|
@@ -7501,11 +7721,11 @@ function log2(quiet, ...args) {
|
|
|
7501
7721
|
function discoverGitRepos(parentDir) {
|
|
7502
7722
|
const repos = [];
|
|
7503
7723
|
try {
|
|
7504
|
-
const entries =
|
|
7724
|
+
const entries = fs29.readdirSync(parentDir, { withFileTypes: true });
|
|
7505
7725
|
for (const entry of entries) {
|
|
7506
7726
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
7507
|
-
const childPath =
|
|
7508
|
-
if (
|
|
7727
|
+
const childPath = path23.join(parentDir, entry.name);
|
|
7728
|
+
if (fs29.existsSync(path23.join(childPath, ".git"))) {
|
|
7509
7729
|
repos.push(childPath);
|
|
7510
7730
|
}
|
|
7511
7731
|
}
|
|
@@ -7529,6 +7749,7 @@ async function refreshSingleRepo(repoDir, options) {
|
|
|
7529
7749
|
}
|
|
7530
7750
|
const spinner = quiet ? null : ora5(`${prefix}Analyzing changes...`).start();
|
|
7531
7751
|
const existingDocs = readExistingConfigs(repoDir);
|
|
7752
|
+
const learnedSection = readLearnedSection();
|
|
7532
7753
|
const fingerprint = await collectFingerprint(repoDir);
|
|
7533
7754
|
const projectContext = {
|
|
7534
7755
|
languages: fingerprint.languages,
|
|
@@ -7544,7 +7765,8 @@ async function refreshSingleRepo(repoDir, options) {
|
|
|
7544
7765
|
summary: diff.summary
|
|
7545
7766
|
},
|
|
7546
7767
|
existingDocs,
|
|
7547
|
-
projectContext
|
|
7768
|
+
projectContext,
|
|
7769
|
+
learnedSection
|
|
7548
7770
|
);
|
|
7549
7771
|
if (!response.docsUpdated || response.docsUpdated.length === 0) {
|
|
7550
7772
|
spinner?.succeed(`${prefix}No doc updates needed`);
|
|
@@ -7606,7 +7828,7 @@ async function refreshCommand(options) {
|
|
|
7606
7828
|
`));
|
|
7607
7829
|
const originalDir = process.cwd();
|
|
7608
7830
|
for (const repo of repos) {
|
|
7609
|
-
const repoName =
|
|
7831
|
+
const repoName = path23.basename(repo);
|
|
7610
7832
|
try {
|
|
7611
7833
|
process.chdir(repo);
|
|
7612
7834
|
await refreshSingleRepo(repo, { ...options, label: repoName });
|
|
@@ -7830,6 +8052,7 @@ async function configCommand() {
|
|
|
7830
8052
|
}
|
|
7831
8053
|
|
|
7832
8054
|
// src/commands/learn.ts
|
|
8055
|
+
import fs31 from "fs";
|
|
7833
8056
|
import chalk17 from "chalk";
|
|
7834
8057
|
|
|
7835
8058
|
// src/learner/stdin.ts
|
|
@@ -7861,8 +8084,8 @@ function readStdin() {
|
|
|
7861
8084
|
|
|
7862
8085
|
// src/learner/storage.ts
|
|
7863
8086
|
init_constants();
|
|
7864
|
-
import
|
|
7865
|
-
import
|
|
8087
|
+
import fs30 from "fs";
|
|
8088
|
+
import path24 from "path";
|
|
7866
8089
|
var MAX_RESPONSE_LENGTH = 2e3;
|
|
7867
8090
|
var DEFAULT_STATE = {
|
|
7868
8091
|
sessionId: null,
|
|
@@ -7870,15 +8093,15 @@ var DEFAULT_STATE = {
|
|
|
7870
8093
|
lastAnalysisTimestamp: null
|
|
7871
8094
|
};
|
|
7872
8095
|
function ensureLearningDir() {
|
|
7873
|
-
if (!
|
|
7874
|
-
|
|
8096
|
+
if (!fs30.existsSync(LEARNING_DIR)) {
|
|
8097
|
+
fs30.mkdirSync(LEARNING_DIR, { recursive: true });
|
|
7875
8098
|
}
|
|
7876
8099
|
}
|
|
7877
8100
|
function sessionFilePath() {
|
|
7878
|
-
return
|
|
8101
|
+
return path24.join(LEARNING_DIR, LEARNING_SESSION_FILE);
|
|
7879
8102
|
}
|
|
7880
8103
|
function stateFilePath() {
|
|
7881
|
-
return
|
|
8104
|
+
return path24.join(LEARNING_DIR, LEARNING_STATE_FILE);
|
|
7882
8105
|
}
|
|
7883
8106
|
function truncateResponse(response) {
|
|
7884
8107
|
const str = JSON.stringify(response);
|
|
@@ -7889,113 +8112,84 @@ function appendEvent(event) {
|
|
|
7889
8112
|
ensureLearningDir();
|
|
7890
8113
|
const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
|
|
7891
8114
|
const filePath = sessionFilePath();
|
|
7892
|
-
|
|
8115
|
+
fs30.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
|
|
7893
8116
|
const count = getEventCount();
|
|
7894
8117
|
if (count > LEARNING_MAX_EVENTS) {
|
|
7895
|
-
const lines =
|
|
8118
|
+
const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
7896
8119
|
const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
|
|
7897
|
-
|
|
8120
|
+
fs30.writeFileSync(filePath, kept.join("\n") + "\n");
|
|
7898
8121
|
}
|
|
7899
8122
|
}
|
|
7900
8123
|
function readAllEvents() {
|
|
7901
8124
|
const filePath = sessionFilePath();
|
|
7902
|
-
if (!
|
|
7903
|
-
const lines =
|
|
8125
|
+
if (!fs30.existsSync(filePath)) return [];
|
|
8126
|
+
const lines = fs30.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
7904
8127
|
return lines.map((line) => JSON.parse(line));
|
|
7905
8128
|
}
|
|
7906
8129
|
function getEventCount() {
|
|
7907
8130
|
const filePath = sessionFilePath();
|
|
7908
|
-
if (!
|
|
7909
|
-
const content =
|
|
8131
|
+
if (!fs30.existsSync(filePath)) return 0;
|
|
8132
|
+
const content = fs30.readFileSync(filePath, "utf-8");
|
|
7910
8133
|
return content.split("\n").filter(Boolean).length;
|
|
7911
8134
|
}
|
|
7912
8135
|
function clearSession() {
|
|
7913
8136
|
const filePath = sessionFilePath();
|
|
7914
|
-
if (
|
|
8137
|
+
if (fs30.existsSync(filePath)) fs30.unlinkSync(filePath);
|
|
7915
8138
|
}
|
|
7916
8139
|
function readState2() {
|
|
7917
8140
|
const filePath = stateFilePath();
|
|
7918
|
-
if (!
|
|
8141
|
+
if (!fs30.existsSync(filePath)) return { ...DEFAULT_STATE };
|
|
7919
8142
|
try {
|
|
7920
|
-
return JSON.parse(
|
|
8143
|
+
return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
|
|
7921
8144
|
} catch {
|
|
7922
8145
|
return { ...DEFAULT_STATE };
|
|
7923
8146
|
}
|
|
7924
8147
|
}
|
|
7925
8148
|
function writeState2(state) {
|
|
7926
8149
|
ensureLearningDir();
|
|
7927
|
-
|
|
8150
|
+
fs30.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
|
|
7928
8151
|
}
|
|
7929
8152
|
function resetState() {
|
|
7930
8153
|
writeState2({ ...DEFAULT_STATE });
|
|
7931
8154
|
}
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
const
|
|
7940
|
-
if (
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
written.push(skillPath);
|
|
8155
|
+
var LOCK_FILE2 = "finalize.lock";
|
|
8156
|
+
var LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
8157
|
+
function lockFilePath() {
|
|
8158
|
+
return path24.join(LEARNING_DIR, LOCK_FILE2);
|
|
8159
|
+
}
|
|
8160
|
+
function acquireFinalizeLock() {
|
|
8161
|
+
ensureLearningDir();
|
|
8162
|
+
const lockPath = lockFilePath();
|
|
8163
|
+
if (fs30.existsSync(lockPath)) {
|
|
8164
|
+
try {
|
|
8165
|
+
const stat = fs30.statSync(lockPath);
|
|
8166
|
+
if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
|
|
8167
|
+
return false;
|
|
8168
|
+
}
|
|
8169
|
+
} catch {
|
|
7948
8170
|
}
|
|
7949
8171
|
}
|
|
7950
|
-
|
|
7951
|
-
}
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
let updated;
|
|
7964
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
7965
|
-
updated = existing.slice(0, startIdx) + section + existing.slice(endIdx + LEARNED_END.length);
|
|
7966
|
-
} else {
|
|
7967
|
-
const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
|
|
7968
|
-
updated = existing + separator + "\n" + section + "\n";
|
|
8172
|
+
try {
|
|
8173
|
+
fs30.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
8174
|
+
return true;
|
|
8175
|
+
} catch {
|
|
8176
|
+
try {
|
|
8177
|
+
const stat = fs30.statSync(lockPath);
|
|
8178
|
+
if (Date.now() - stat.mtimeMs >= LOCK_STALE_MS) {
|
|
8179
|
+
fs30.writeFileSync(lockPath, String(process.pid));
|
|
8180
|
+
return true;
|
|
8181
|
+
}
|
|
8182
|
+
} catch {
|
|
8183
|
+
}
|
|
8184
|
+
return false;
|
|
7969
8185
|
}
|
|
7970
|
-
fs30.writeFileSync(claudeMdPath, updated);
|
|
7971
8186
|
}
|
|
7972
|
-
function
|
|
7973
|
-
const
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
const existing = fs30.readFileSync(skillPath, "utf-8");
|
|
7978
|
-
fs30.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
|
|
7979
|
-
} else {
|
|
7980
|
-
const frontmatter = [
|
|
7981
|
-
"---",
|
|
7982
|
-
`name: ${skill.name}`,
|
|
7983
|
-
`description: ${skill.description}`,
|
|
7984
|
-
"---",
|
|
7985
|
-
""
|
|
7986
|
-
].join("\n");
|
|
7987
|
-
fs30.writeFileSync(skillPath, frontmatter + skill.content);
|
|
8187
|
+
function releaseFinalizeLock() {
|
|
8188
|
+
const lockPath = lockFilePath();
|
|
8189
|
+
try {
|
|
8190
|
+
if (fs30.existsSync(lockPath)) fs30.unlinkSync(lockPath);
|
|
8191
|
+
} catch {
|
|
7988
8192
|
}
|
|
7989
|
-
return skillPath;
|
|
7990
|
-
}
|
|
7991
|
-
function readLearnedSection() {
|
|
7992
|
-
const claudeMdPath = "CLAUDE.md";
|
|
7993
|
-
if (!fs30.existsSync(claudeMdPath)) return null;
|
|
7994
|
-
const content = fs30.readFileSync(claudeMdPath, "utf-8");
|
|
7995
|
-
const startIdx = content.indexOf(LEARNED_START);
|
|
7996
|
-
const endIdx = content.indexOf(LEARNED_END);
|
|
7997
|
-
if (startIdx === -1 || endIdx === -1) return null;
|
|
7998
|
-
return content.slice(startIdx + LEARNED_START.length, endIdx).trim();
|
|
7999
8193
|
}
|
|
8000
8194
|
|
|
8001
8195
|
// src/ai/learn.ts
|
|
@@ -8074,6 +8268,7 @@ ${eventsText}`;
|
|
|
8074
8268
|
|
|
8075
8269
|
// src/commands/learn.ts
|
|
8076
8270
|
init_config();
|
|
8271
|
+
var MIN_EVENTS_FOR_ANALYSIS = 50;
|
|
8077
8272
|
async function learnObserveCommand(options) {
|
|
8078
8273
|
try {
|
|
8079
8274
|
const raw = await readStdin();
|
|
@@ -8081,11 +8276,11 @@ async function learnObserveCommand(options) {
|
|
|
8081
8276
|
const hookData = JSON.parse(raw);
|
|
8082
8277
|
const event = {
|
|
8083
8278
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8084
|
-
session_id: hookData.session_id || "unknown",
|
|
8279
|
+
session_id: hookData.session_id || hookData.conversation_id || "unknown",
|
|
8085
8280
|
hook_event_name: options.failure ? "PostToolUseFailure" : "PostToolUse",
|
|
8086
8281
|
tool_name: hookData.tool_name || "unknown",
|
|
8087
8282
|
tool_input: hookData.tool_input || {},
|
|
8088
|
-
tool_response: hookData.tool_response || {},
|
|
8283
|
+
tool_response: hookData.tool_response || hookData.tool_output || {},
|
|
8089
8284
|
tool_use_id: hookData.tool_use_id || "",
|
|
8090
8285
|
cwd: hookData.cwd || process.cwd()
|
|
8091
8286
|
};
|
|
@@ -8100,6 +8295,8 @@ async function learnObserveCommand(options) {
|
|
|
8100
8295
|
async function learnFinalizeCommand() {
|
|
8101
8296
|
const { isCaliberRunning: isCaliberRunning2 } = await Promise.resolve().then(() => (init_lock(), lock_exports));
|
|
8102
8297
|
if (isCaliberRunning2()) return;
|
|
8298
|
+
if (!acquireFinalizeLock()) return;
|
|
8299
|
+
let analyzed = false;
|
|
8103
8300
|
try {
|
|
8104
8301
|
const config = loadConfig();
|
|
8105
8302
|
if (!config) {
|
|
@@ -8108,12 +8305,9 @@ async function learnFinalizeCommand() {
|
|
|
8108
8305
|
return;
|
|
8109
8306
|
}
|
|
8110
8307
|
const events = readAllEvents();
|
|
8111
|
-
if (
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
return;
|
|
8115
|
-
}
|
|
8116
|
-
await validateModel();
|
|
8308
|
+
if (events.length < MIN_EVENTS_FOR_ANALYSIS) return;
|
|
8309
|
+
await validateModel({ fast: true });
|
|
8310
|
+
migrateInlineLearnings();
|
|
8117
8311
|
const existingConfigs = readExistingConfigs(process.cwd());
|
|
8118
8312
|
const existingLearnedSection = readLearnedSection();
|
|
8119
8313
|
const existingSkills = existingConfigs.claudeSkills || [];
|
|
@@ -8123,51 +8317,97 @@ async function learnFinalizeCommand() {
|
|
|
8123
8317
|
existingLearnedSection,
|
|
8124
8318
|
existingSkills
|
|
8125
8319
|
);
|
|
8320
|
+
analyzed = true;
|
|
8126
8321
|
if (response.claudeMdLearnedSection || response.skills?.length) {
|
|
8127
|
-
writeLearnedContent({
|
|
8322
|
+
const result = writeLearnedContent({
|
|
8128
8323
|
claudeMdLearnedSection: response.claudeMdLearnedSection,
|
|
8129
8324
|
skills: response.skills
|
|
8130
8325
|
});
|
|
8326
|
+
if (result.newItemCount > 0) {
|
|
8327
|
+
console.log(chalk17.dim(`caliber: learned ${result.newItemCount} new pattern${result.newItemCount === 1 ? "" : "s"}`));
|
|
8328
|
+
for (const item of result.newItems) {
|
|
8329
|
+
console.log(chalk17.dim(` + ${item.replace(/^- /, "").slice(0, 80)}`));
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8131
8332
|
}
|
|
8132
8333
|
} catch {
|
|
8133
8334
|
} finally {
|
|
8134
|
-
|
|
8135
|
-
|
|
8335
|
+
if (analyzed) {
|
|
8336
|
+
clearSession();
|
|
8337
|
+
resetState();
|
|
8338
|
+
}
|
|
8339
|
+
releaseFinalizeLock();
|
|
8136
8340
|
}
|
|
8137
8341
|
}
|
|
8138
8342
|
async function learnInstallCommand() {
|
|
8139
|
-
|
|
8140
|
-
if (
|
|
8141
|
-
|
|
8343
|
+
let anyInstalled = false;
|
|
8344
|
+
if (fs31.existsSync(".claude")) {
|
|
8345
|
+
const r = installLearningHooks();
|
|
8346
|
+
if (r.installed) {
|
|
8347
|
+
console.log(chalk17.green("\u2713") + " Claude Code learning hooks installed");
|
|
8348
|
+
anyInstalled = true;
|
|
8349
|
+
} else if (r.alreadyInstalled) {
|
|
8350
|
+
console.log(chalk17.dim(" Claude Code hooks already installed"));
|
|
8351
|
+
}
|
|
8352
|
+
}
|
|
8353
|
+
if (fs31.existsSync(".cursor")) {
|
|
8354
|
+
const r = installCursorLearningHooks();
|
|
8355
|
+
if (r.installed) {
|
|
8356
|
+
console.log(chalk17.green("\u2713") + " Cursor learning hooks installed");
|
|
8357
|
+
anyInstalled = true;
|
|
8358
|
+
} else if (r.alreadyInstalled) {
|
|
8359
|
+
console.log(chalk17.dim(" Cursor hooks already installed"));
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
if (!fs31.existsSync(".claude") && !fs31.existsSync(".cursor")) {
|
|
8363
|
+
console.log(chalk17.yellow("No .claude/ or .cursor/ directory found."));
|
|
8364
|
+
console.log(chalk17.dim(" Run `caliber init` first, or create the directory manually."));
|
|
8142
8365
|
return;
|
|
8143
8366
|
}
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8367
|
+
if (anyInstalled) {
|
|
8368
|
+
console.log(chalk17.dim(` Tool usage will be recorded and learnings extracted after \u2265${MIN_EVENTS_FOR_ANALYSIS} events.`));
|
|
8369
|
+
console.log(chalk17.dim(" Learnings written to CALIBER_LEARNINGS.md."));
|
|
8370
|
+
}
|
|
8147
8371
|
}
|
|
8148
8372
|
async function learnRemoveCommand() {
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8373
|
+
let anyRemoved = false;
|
|
8374
|
+
const r1 = removeLearningHooks();
|
|
8375
|
+
if (r1.removed) {
|
|
8376
|
+
console.log(chalk17.green("\u2713") + " Claude Code learning hooks removed");
|
|
8377
|
+
anyRemoved = true;
|
|
8378
|
+
}
|
|
8379
|
+
const r2 = removeCursorLearningHooks();
|
|
8380
|
+
if (r2.removed) {
|
|
8381
|
+
console.log(chalk17.green("\u2713") + " Cursor learning hooks removed");
|
|
8382
|
+
anyRemoved = true;
|
|
8383
|
+
}
|
|
8384
|
+
if (!anyRemoved) {
|
|
8385
|
+
console.log(chalk17.dim("No learning hooks found."));
|
|
8153
8386
|
}
|
|
8154
|
-
console.log(chalk17.green("\u2713") + " Learning hooks removed from .claude/settings.json");
|
|
8155
8387
|
}
|
|
8156
8388
|
async function learnStatusCommand() {
|
|
8157
|
-
const
|
|
8389
|
+
const claudeInstalled = areLearningHooksInstalled();
|
|
8390
|
+
const cursorInstalled = areCursorLearningHooksInstalled();
|
|
8158
8391
|
const state = readState2();
|
|
8159
8392
|
const eventCount = getEventCount();
|
|
8160
8393
|
console.log(chalk17.bold("Session Learning Status"));
|
|
8161
8394
|
console.log();
|
|
8162
|
-
if (
|
|
8163
|
-
console.log(chalk17.green("\u2713") + "
|
|
8395
|
+
if (claudeInstalled) {
|
|
8396
|
+
console.log(chalk17.green("\u2713") + " Claude Code hooks " + chalk17.green("installed"));
|
|
8397
|
+
} else {
|
|
8398
|
+
console.log(chalk17.dim("\u2717") + " Claude Code hooks " + chalk17.dim("not installed"));
|
|
8399
|
+
}
|
|
8400
|
+
if (cursorInstalled) {
|
|
8401
|
+
console.log(chalk17.green("\u2713") + " Cursor hooks " + chalk17.green("installed"));
|
|
8164
8402
|
} else {
|
|
8165
|
-
console.log(chalk17.dim("\u2717") + "
|
|
8403
|
+
console.log(chalk17.dim("\u2717") + " Cursor hooks " + chalk17.dim("not installed"));
|
|
8404
|
+
}
|
|
8405
|
+
if (!claudeInstalled && !cursorInstalled) {
|
|
8166
8406
|
console.log(chalk17.dim(" Run `caliber learn install` to enable session learning."));
|
|
8167
8407
|
}
|
|
8168
8408
|
console.log();
|
|
8169
8409
|
console.log(`Events recorded: ${chalk17.cyan(String(eventCount))}`);
|
|
8170
|
-
console.log(`
|
|
8410
|
+
console.log(`Threshold for analysis: ${chalk17.cyan(String(MIN_EVENTS_FOR_ANALYSIS))}`);
|
|
8171
8411
|
if (state.lastAnalysisTimestamp) {
|
|
8172
8412
|
console.log(`Last analysis: ${chalk17.cyan(state.lastAnalysisTimestamp)}`);
|
|
8173
8413
|
} else {
|
|
@@ -8177,14 +8417,14 @@ async function learnStatusCommand() {
|
|
|
8177
8417
|
if (learnedSection) {
|
|
8178
8418
|
const lineCount = learnedSection.split("\n").filter(Boolean).length;
|
|
8179
8419
|
console.log(`
|
|
8180
|
-
Learned items in
|
|
8420
|
+
Learned items in CALIBER_LEARNINGS.md: ${chalk17.cyan(String(lineCount))}`);
|
|
8181
8421
|
}
|
|
8182
8422
|
}
|
|
8183
8423
|
|
|
8184
8424
|
// src/cli.ts
|
|
8185
8425
|
var __dirname = path25.dirname(fileURLToPath(import.meta.url));
|
|
8186
8426
|
var pkg = JSON.parse(
|
|
8187
|
-
|
|
8427
|
+
fs32.readFileSync(path25.resolve(__dirname, "..", "package.json"), "utf-8")
|
|
8188
8428
|
);
|
|
8189
8429
|
var program = new Command();
|
|
8190
8430
|
var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
|
|
@@ -8258,7 +8498,7 @@ learn.command("remove").description("Remove learning hooks from .claude/settings
|
|
|
8258
8498
|
learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
|
|
8259
8499
|
|
|
8260
8500
|
// src/utils/version-check.ts
|
|
8261
|
-
import
|
|
8501
|
+
import fs33 from "fs";
|
|
8262
8502
|
import path26 from "path";
|
|
8263
8503
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8264
8504
|
import { execSync as execSync14 } from "child_process";
|
|
@@ -8267,13 +8507,13 @@ import ora6 from "ora";
|
|
|
8267
8507
|
import confirm2 from "@inquirer/confirm";
|
|
8268
8508
|
var __dirname_vc = path26.dirname(fileURLToPath2(import.meta.url));
|
|
8269
8509
|
var pkg2 = JSON.parse(
|
|
8270
|
-
|
|
8510
|
+
fs33.readFileSync(path26.resolve(__dirname_vc, "..", "package.json"), "utf-8")
|
|
8271
8511
|
);
|
|
8272
8512
|
function getInstalledVersion() {
|
|
8273
8513
|
try {
|
|
8274
8514
|
const globalRoot = execSync14("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8275
8515
|
const pkgPath = path26.join(globalRoot, "@rely-ai", "caliber", "package.json");
|
|
8276
|
-
return JSON.parse(
|
|
8516
|
+
return JSON.parse(fs33.readFileSync(pkgPath, "utf-8")).version;
|
|
8277
8517
|
} catch {
|
|
8278
8518
|
return null;
|
|
8279
8519
|
}
|
package/package.json
CHANGED