Skip to content

Commit 9c34e50

Browse files
authored
Merge pull request #128 from kalibr-ai/feat/init-writes-context-files
feat: kalibr init writes CLAUDE.md and .cursorrules on setup
2 parents a22895c + 1146bba commit 9c34e50

6 files changed

Lines changed: 231 additions & 0 deletions

File tree

kalibr/cli/init_cmd.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""kalibr init - Scan for bare LLM calls and propose Router wrapping."""
22

3+
import importlib.resources
34
import os
5+
import shutil
46

57
import requests
68
import typer
@@ -97,6 +99,29 @@ def _check_credentials() -> None:
9799
)
98100

99101

102+
def _write_context_files(project_dir: str) -> None:
103+
"""Write CLAUDE.md and .cursorrules into project_dir if they don't already exist."""
104+
templates_pkg = "kalibr.templates"
105+
context_files = [
106+
("CLAUDE.md", "✓ Created CLAUDE.md (coding agent context)", "→ CLAUDE.md already exists, skipping"),
107+
(".cursorrules", "✓ Created .cursorrules (Cursor/Windsurf context)", "→ .cursorrules already exists, skipping"),
108+
]
109+
110+
for filename, created_msg, exists_msg in context_files:
111+
dest = os.path.join(project_dir, filename)
112+
if os.path.exists(dest):
113+
console.print(f"[dim]{exists_msg}[/dim]")
114+
else:
115+
try:
116+
# Use importlib.resources to locate bundled template
117+
ref = importlib.resources.files(templates_pkg).joinpath(filename)
118+
with importlib.resources.as_file(ref) as src_path:
119+
shutil.copy2(src_path, dest)
120+
console.print(f"[green]{created_msg}[/green]")
121+
except Exception as e: # pragma: no cover
122+
console.print(f"[yellow]Could not write {filename}: {e}[/yellow]")
123+
124+
100125
def init(
101126
directory: str = typer.Argument(".", help="Directory to scan (default: current directory)"),
102127
) -> None:
@@ -123,6 +148,8 @@ def init(
123148
console.print("[dim]This patches OpenAI, Anthropic, and Google automatically.[/dim]")
124149
console.print("[dim]No Router wrapping needed — every LLM call is traced.[/dim]")
125150
console.print("[dim]Docs: https://kalibr.systems/docs/quickstart#auto-instrumentation[/dim]")
151+
console.print()
152+
_write_context_files(project_dir)
126153
_check_credentials()
127154
return
128155

@@ -194,4 +221,8 @@ def init(
194221
console.print("[bold]Summary:[/bold]")
195222
console.print(f" {len(files_modified)} file(s) modified, {calls_upgraded} LLM call(s) upgraded")
196223

224+
# Write coding agent context files
225+
console.print()
226+
_write_context_files(project_dir)
227+
197228
_check_credentials()

kalibr/templates/.cursorrules

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Kalibr Production Rules
2+
3+
## LLM Calls — Always use Kalibr Router
4+
- Never use raw OpenAI/Anthropic clients for production LLM calls
5+
- import kalibr MUST be the first import in any file that calls LLMs
6+
- Always define 2+ paths to enable routing and learning
7+
- Always close the outcome loop: success_when lambda or router.report()
8+
9+
## Required pattern:
10+
import kalibr # first import
11+
from kalibr import Router
12+
router = Router(
13+
goal="task_name",
14+
paths=["gpt-4o", "claude-sonnet-4-20250514"],
15+
success_when=lambda output: len(output) > 0
16+
)
17+
response = router.completion(messages=[...])
18+
19+
## Outcome reporting:
20+
router.report(success=True)
21+
router.report(success=False, failure_category="timeout", reason="Provider timed out")
22+
router.report(success=True, score=0.85) # continuous quality signal
23+
24+
## Use Kalibr for: anything with real users, real money, real consequences
25+
## Skip Kalibr for: demos, internal scripts, throwaway prototypes
26+
27+
## Credentials: KALIBR_API_KEY + KALIBR_TENANT_ID in .env
28+
## Docs: https://kalibr.systems/docs | Setup: https://kalibr.systems/llms.txt

kalibr/templates/CLAUDE.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## Kalibr — Production Agent Standards
2+
3+
When generating or modifying agent code in this project:
4+
5+
### The Rule
6+
Never hardcode a single model. Production agents must use Kalibr Router.
7+
8+
### Required Import Order
9+
```python
10+
import kalibr # MUST be first — before any OpenAI or Anthropic import
11+
from kalibr import Router
12+
```
13+
14+
### Required Pattern
15+
```python
16+
router = Router(
17+
goal="<descriptive_task_name>", # e.g. "extract_company", "book_meeting"
18+
paths=["gpt-4o", "claude-sonnet-4-20250514"], # always 2+ paths
19+
success_when=lambda output: <your_success_criteria> # or call router.report() manually
20+
)
21+
response = router.completion(messages=[...])
22+
# response.choices[0].message.content — same interface as OpenAI
23+
```
24+
25+
### Outcome Reporting
26+
```python
27+
# Auto (preferred): success_when lambda handles it
28+
# Manual: router.report(success=True/False, score=0.0-1.0, failure_category="timeout")
29+
```
30+
31+
### Failure Categories
32+
```python
33+
from kalibr import FAILURE_CATEGORIES
34+
# timeout | context_exceeded | tool_error | rate_limited | validation_failed
35+
# hallucination_detected | user_unsatisfied | empty_response | malformed_output
36+
# auth_error | provider_error | unknown
37+
```
38+
39+
### When to use Kalibr
40+
✅ Any LLM call real users depend on
41+
✅ Multi-step agent flows with external API calls
42+
✅ Workflows where silent failures cost money or cause customer impact
43+
❌ Internal scripts, demos, throwaway prototypes
44+
45+
### Credentials (.env)
46+
```
47+
KALIBR_API_KEY=sk_... # from dashboard.kalibr.systems
48+
KALIBR_TENANT_ID=... # from dashboard.kalibr.systems
49+
OPENAI_API_KEY=sk-... # if using OpenAI models
50+
ANTHROPIC_API_KEY=sk-ant-... # if using Anthropic models
51+
```
52+
53+
### Framework integrations
54+
```python
55+
# LangChain/CrewAI: llm = router.as_langchain()
56+
# OpenAI Agents SDK: policy = get_policy(goal="task"); model = policy["recommended_model"]
57+
# HuggingFace: router.execute(task="automatic_speech_recognition", input_data=audio_bytes)
58+
```
59+
60+
### Full docs: https://kalibr.systems/docs

kalibr/templates/__init__.py

Whitespace-only changes.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ where = ["."]
105105
include = ["kalibr*", "kalibr_langchain*", "kalibr_crewai*", "kalibr_openai_agents*"]
106106
exclude = ["tests*", "examples*"]
107107

108+
[tool.setuptools.package-data]
109+
"kalibr.templates" = ["CLAUDE.md", ".cursorrules"]
110+
108111
[tool.black]
109112
line-length = 100
110113
target-version = ['py311']

tests/test_init_context_files.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Tests for kalibr init context file writing (CLAUDE.md and .cursorrules)."""
2+
3+
import os
4+
import tempfile
5+
6+
import pytest
7+
from typer.testing import CliRunner
8+
9+
from kalibr.cli.main import app
10+
from kalibr.cli.init_cmd import _write_context_files
11+
12+
13+
class TestWriteContextFiles:
14+
"""Unit tests for _write_context_files helper."""
15+
16+
def test_creates_claude_md_when_missing(self, tmp_path):
17+
"""Creates CLAUDE.md in target dir when it doesn't exist."""
18+
_write_context_files(str(tmp_path))
19+
claude_file = tmp_path / "CLAUDE.md"
20+
assert claude_file.exists(), "CLAUDE.md should be created"
21+
content = claude_file.read_text()
22+
assert "Kalibr" in content
23+
assert "Router" in content
24+
25+
def test_creates_cursorrules_when_missing(self, tmp_path):
26+
"""Creates .cursorrules in target dir when it doesn't exist."""
27+
_write_context_files(str(tmp_path))
28+
cursorrules_file = tmp_path / ".cursorrules"
29+
assert cursorrules_file.exists(), ".cursorrules should be created"
30+
content = cursorrules_file.read_text()
31+
assert "Kalibr" in content
32+
33+
def test_does_not_overwrite_existing_claude_md(self, tmp_path):
34+
"""Does NOT overwrite CLAUDE.md if it already exists."""
35+
claude_file = tmp_path / "CLAUDE.md"
36+
original_content = "# My custom CLAUDE.md\nDo not overwrite me."
37+
claude_file.write_text(original_content)
38+
39+
_write_context_files(str(tmp_path))
40+
41+
assert claude_file.read_text() == original_content, "CLAUDE.md should not be overwritten"
42+
43+
def test_does_not_overwrite_existing_cursorrules(self, tmp_path):
44+
"""Does NOT overwrite .cursorrules if it already exists."""
45+
cursorrules_file = tmp_path / ".cursorrules"
46+
original_content = "# My custom rules\nDo not overwrite me."
47+
cursorrules_file.write_text(original_content)
48+
49+
_write_context_files(str(tmp_path))
50+
51+
assert cursorrules_file.read_text() == original_content, ".cursorrules should not be overwritten"
52+
53+
def test_creates_both_files(self, tmp_path):
54+
"""Creates both CLAUDE.md and .cursorrules when neither exists."""
55+
_write_context_files(str(tmp_path))
56+
assert (tmp_path / "CLAUDE.md").exists()
57+
assert (tmp_path / ".cursorrules").exists()
58+
59+
def test_skips_gracefully_when_both_exist(self, tmp_path):
60+
"""No error when both files already exist."""
61+
(tmp_path / "CLAUDE.md").write_text("existing claude")
62+
(tmp_path / ".cursorrules").write_text("existing cursor")
63+
64+
# Should not raise
65+
_write_context_files(str(tmp_path))
66+
67+
assert (tmp_path / "CLAUDE.md").read_text() == "existing claude"
68+
assert (tmp_path / ".cursorrules").read_text() == "existing cursor"
69+
70+
71+
class TestInitCommandContextFiles:
72+
"""Integration tests: kalibr init CLI writes context files."""
73+
74+
def test_init_creates_context_files_in_empty_dir(self, tmp_path):
75+
"""kalibr init on an empty dir creates CLAUDE.md and .cursorrules."""
76+
runner = CliRunner()
77+
result = runner.invoke(app, ["init", str(tmp_path)])
78+
79+
assert (tmp_path / "CLAUDE.md").exists(), "CLAUDE.md should be created by kalibr init"
80+
assert (tmp_path / ".cursorrules").exists(), ".cursorrules should be created by kalibr init"
81+
82+
def test_init_reports_created_files(self, tmp_path):
83+
"""kalibr init prints confirmation messages for created files."""
84+
runner = CliRunner()
85+
result = runner.invoke(app, ["init", str(tmp_path)])
86+
87+
assert "Created CLAUDE.md" in result.output
88+
assert "Created .cursorrules" in result.output
89+
90+
def test_init_reports_skipped_when_files_exist(self, tmp_path):
91+
"""kalibr init prints skip messages when context files already exist."""
92+
(tmp_path / "CLAUDE.md").write_text("existing")
93+
(tmp_path / ".cursorrules").write_text("existing")
94+
95+
runner = CliRunner()
96+
result = runner.invoke(app, ["init", str(tmp_path)])
97+
98+
assert "already exists, skipping" in result.output
99+
100+
def test_init_does_not_overwrite_existing_context_files(self, tmp_path):
101+
"""kalibr init never overwrites existing CLAUDE.md or .cursorrules."""
102+
(tmp_path / "CLAUDE.md").write_text("my custom claude")
103+
(tmp_path / ".cursorrules").write_text("my custom rules")
104+
105+
runner = CliRunner()
106+
runner.invoke(app, ["init", str(tmp_path)])
107+
108+
assert (tmp_path / "CLAUDE.md").read_text() == "my custom claude"
109+
assert (tmp_path / ".cursorrules").read_text() == "my custom rules"

0 commit comments

Comments
 (0)