librarian-code 0.1.0__py3-none-any.whl
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.
- librarian/__init__.py +3 -0
- librarian/__main__.py +3 -0
- librarian/actions/__init__.py +0 -0
- librarian/actions/file_ops.py +47 -0
- librarian/actions/safety.py +29 -0
- librarian/actions/shell_ops.py +49 -0
- librarian/adapter/__init__.py +0 -0
- librarian/adapter/base.py +11 -0
- librarian/adapter/groq_adapter.py +40 -0
- librarian/adapter/openrouter_adapter.py +58 -0
- librarian/cli.py +26 -0
- librarian/commands/__init__.py +0 -0
- librarian/commands/ask.py +46 -0
- librarian/commands/do.py +232 -0
- librarian/commands/init.py +96 -0
- librarian/commands/status.py +71 -0
- librarian/commands/undo.py +85 -0
- librarian/commands/why.py +47 -0
- librarian/exceptions.py +22 -0
- librarian/memory/__init__.py +0 -0
- librarian/memory/capsule.py +94 -0
- librarian/memory/chunker.py +183 -0
- librarian/memory/decision_log.py +36 -0
- librarian/memory/indexer.py +96 -0
- librarian/memory/retriever.py +62 -0
- librarian/orchestrator/__init__.py +0 -0
- librarian/orchestrator/core.py +47 -0
- librarian/orchestrator/router.py +17 -0
- librarian/skills/__init__.py +0 -0
- librarian/skills/bundled/__init__.py +0 -0
- librarian/skills/bundled/api-design/conventions.md +93 -0
- librarian/skills/bundled/python/conventions.md +59 -0
- librarian/skills/bundled/react/conventions.md +83 -0
- librarian/skills/bundled/web-dev/conventions.md +54 -0
- librarian/skills/loader.py +109 -0
- librarian/utils/__init__.py +0 -0
- librarian/utils/config.py +15 -0
- librarian/utils/logger.py +32 -0
- librarian/utils/token_tracker.py +16 -0
- librarian/utils/ui.py +97 -0
- librarian_code-0.1.0.dist-info/METADATA +180 -0
- librarian_code-0.1.0.dist-info/RECORD +45 -0
- librarian_code-0.1.0.dist-info/WHEEL +4 -0
- librarian_code-0.1.0.dist-info/entry_points.txt +2 -0
- librarian_code-0.1.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import chromadb
|
|
3
|
+
from sentence_transformers import SentenceTransformer
|
|
4
|
+
from librarian.utils.config import CHROMA_PERSIST_DIR, EMBED_MODEL
|
|
5
|
+
from librarian.memory.indexer import _sanitize_collection_name
|
|
6
|
+
|
|
7
|
+
_model = None
|
|
8
|
+
_client = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_model():
|
|
12
|
+
global _model
|
|
13
|
+
if _model is None:
|
|
14
|
+
_model = SentenceTransformer(EMBED_MODEL)
|
|
15
|
+
return _model
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_client():
|
|
19
|
+
global _client
|
|
20
|
+
if _client is None:
|
|
21
|
+
_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIR)
|
|
22
|
+
return _client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def retrieve(query: str, n_results: int = 5) -> list[dict]:
|
|
26
|
+
model = _get_model()
|
|
27
|
+
client = _get_client()
|
|
28
|
+
project_name = _sanitize_collection_name(os.path.basename(os.getcwd()))
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
collection = client.get_collection(name=project_name)
|
|
32
|
+
except Exception:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
query_embedding = model.encode([query]).tolist()
|
|
36
|
+
results = collection.query(
|
|
37
|
+
query_embeddings=query_embedding,
|
|
38
|
+
n_results=n_results,
|
|
39
|
+
include=["documents", "metadatas", "distances"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not results.get("documents") or not results["documents"][0]:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
chunks = []
|
|
46
|
+
for doc, meta, dist in zip(
|
|
47
|
+
results["documents"][0],
|
|
48
|
+
results["metadatas"][0],
|
|
49
|
+
results["distances"][0],
|
|
50
|
+
):
|
|
51
|
+
if dist > 2.5:
|
|
52
|
+
continue
|
|
53
|
+
chunks.append({
|
|
54
|
+
"content": doc,
|
|
55
|
+
"metadata": meta,
|
|
56
|
+
"distance": dist,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if chunks and sum(c["distance"] for c in chunks) / len(chunks) > 2.0:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
return chunks
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from librarian.orchestrator.router import get_response
|
|
3
|
+
from librarian.memory.retriever import retrieve
|
|
4
|
+
from librarian.skills.loader import build_skill_context
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_system_prompt(project_conventions: str, skill_context: str = "") -> str:
|
|
8
|
+
parts = [
|
|
9
|
+
"You are Librarian, a CLI coding agent with memory of this project.",
|
|
10
|
+
f"Project conventions:\n{project_conventions}",
|
|
11
|
+
]
|
|
12
|
+
if skill_context:
|
|
13
|
+
parts.append(f"Domain-specific best practices:\n{skill_context}")
|
|
14
|
+
parts.append("""Rules:
|
|
15
|
+
- Always explain what you are about to do before doing it
|
|
16
|
+
- Never delete files without explicit confirmation
|
|
17
|
+
- Prefer editing existing code over rewriting from scratch
|
|
18
|
+
- When uncertain, ask rather than assume
|
|
19
|
+
- Be concise and direct in your answers
|
|
20
|
+
- Cite source files when referencing code (e.g. "in auth.py line 42")
|
|
21
|
+
""")
|
|
22
|
+
return "\n\n".join(parts)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_librarian_md() -> str:
|
|
26
|
+
path = Path("LIBRARIAN.md")
|
|
27
|
+
if path.exists():
|
|
28
|
+
return path.read_text(encoding="utf-8")
|
|
29
|
+
return "No project conventions file found."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ask(question: str) -> tuple[str, str, int]:
|
|
33
|
+
conventions = read_librarian_md()
|
|
34
|
+
skill_ctx = build_skill_context()
|
|
35
|
+
system = build_system_prompt(conventions, skill_ctx)
|
|
36
|
+
|
|
37
|
+
chunks = retrieve(question, n_results=5)
|
|
38
|
+
context = ""
|
|
39
|
+
if chunks:
|
|
40
|
+
parts = []
|
|
41
|
+
for c in chunks:
|
|
42
|
+
meta = c["metadata"]
|
|
43
|
+
parts.append(f"--- {meta['file_path']}:{meta.get('start_line', '?')}-{meta.get('end_line', '?')} ---\n{c['content']}")
|
|
44
|
+
context = "\n\n".join(parts)
|
|
45
|
+
|
|
46
|
+
prompt = f"Relevant code context:\n{context}\n\nQuestion: {question}" if context else question
|
|
47
|
+
return get_response(system, prompt)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from librarian.adapter.groq_adapter import GroqAdapter
|
|
2
|
+
from librarian.adapter.openrouter_adapter import OpenRouterAdapter
|
|
3
|
+
from librarian.exceptions import RateLimitError, ProviderUnavailableError
|
|
4
|
+
from librarian.utils.logger import log_warning
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_response(system: str, prompt: str) -> tuple[str, str, int]:
|
|
8
|
+
primary = GroqAdapter()
|
|
9
|
+
fallback = OpenRouterAdapter()
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
response = primary.complete(system, prompt)
|
|
13
|
+
return response, "groq", primary.tokens_used
|
|
14
|
+
except (RateLimitError, ProviderUnavailableError) as e:
|
|
15
|
+
log_warning(f"{e} — switching to OpenRouter")
|
|
16
|
+
response = fallback.complete(system, prompt)
|
|
17
|
+
return response, "openrouter", fallback.tokens_used
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# API Design Conventions
|
|
2
|
+
|
|
3
|
+
## URL Structure
|
|
4
|
+
- Use nouns, not verbs (HTTP methods are the verbs)
|
|
5
|
+
- Plural nouns for collections: `/users`, `/orders`
|
|
6
|
+
- Hyphens for multi-word segments: `/managed-devices`
|
|
7
|
+
- Lowercase consistently
|
|
8
|
+
- No trailing slashes, no file extensions
|
|
9
|
+
- Limit nesting depth to 3 levels max
|
|
10
|
+
|
|
11
|
+
## HTTP Methods
|
|
12
|
+
| Method | Purpose | Success Code |
|
|
13
|
+
|--------|---------|-------------|
|
|
14
|
+
| GET | Read resource | 200 |
|
|
15
|
+
| POST | Create resource | 201 (with Location header) |
|
|
16
|
+
| PUT | Full replace | 200, 201, 204 |
|
|
17
|
+
| PATCH | Partial update | 200, 204 |
|
|
18
|
+
| DELETE | Remove resource | 200, 204 |
|
|
19
|
+
|
|
20
|
+
- POST to collection → 201 Created + Location header
|
|
21
|
+
- PUT is idempotent, POST is not
|
|
22
|
+
- Use PATCH for partial updates, not PUT
|
|
23
|
+
|
|
24
|
+
## Status Codes
|
|
25
|
+
- 200: Generic success (GET, PUT, PATCH)
|
|
26
|
+
- 201: Resource created (POST) — always include Location header
|
|
27
|
+
- 204: Success with no body (DELETE)
|
|
28
|
+
- 400: Bad request / validation error
|
|
29
|
+
- 401: Not authenticated (missing/invalid credentials)
|
|
30
|
+
- 403: Not authorized (insufficient permissions)
|
|
31
|
+
- 404: Resource not found
|
|
32
|
+
- 409: Conflict (duplicate creation)
|
|
33
|
+
- 422: Semantically invalid input
|
|
34
|
+
- 429: Rate limited — include Retry-After header
|
|
35
|
+
- 500: Server error
|
|
36
|
+
|
|
37
|
+
401 ≠ 403: 401 = not logged in, 403 = logged in but no permission
|
|
38
|
+
|
|
39
|
+
## Request/Response Patterns
|
|
40
|
+
- Always use `Content-Type: application/json`
|
|
41
|
+
- Collection responses: `{ "data": [...], "meta": { "total", "page", "hasNext" } }`
|
|
42
|
+
- Single resource: `{ "id": 1, "name": "..." }`
|
|
43
|
+
- Creation response: 201 + Location header + resource body
|
|
44
|
+
- Error response: `{ "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [...] } }`
|
|
45
|
+
- Never expose stack traces or internal errors in production
|
|
46
|
+
|
|
47
|
+
## Pagination
|
|
48
|
+
- Offset-based: `?page=2&limit=25` (simple)
|
|
49
|
+
- Cursor-based: `?after=<cursor>&first=25` (preferred for large datasets)
|
|
50
|
+
- Always include: total, hasNext/hasPrev
|
|
51
|
+
|
|
52
|
+
## Filtering & Sorting
|
|
53
|
+
- Filter via query params: `?category=electronics&status=active`
|
|
54
|
+
- Sort: `sort=field` (asc), `sort=-field` (desc)
|
|
55
|
+
- Search: `?search=john`
|
|
56
|
+
|
|
57
|
+
## Authentication
|
|
58
|
+
- JWT: `Authorization: Bearer <token>` header
|
|
59
|
+
- API Key: `X-API-Key: <key>` header (never in URLs)
|
|
60
|
+
- Always use HTTPS
|
|
61
|
+
- Hash passwords with bcrypt/scrypt
|
|
62
|
+
- Token refresh: short-lived access + long-lived refresh
|
|
63
|
+
|
|
64
|
+
## Rate Limiting
|
|
65
|
+
- Return 429 with headers: `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`
|
|
66
|
+
- Implement per-user, per-IP, or per-API-key
|
|
67
|
+
|
|
68
|
+
## CORS
|
|
69
|
+
- Set explicit origins (never `*` with credentials)
|
|
70
|
+
- Handle OPTIONS preflight
|
|
71
|
+
- Return CORS headers on error responses too
|
|
72
|
+
|
|
73
|
+
## Folder Structure
|
|
74
|
+
```
|
|
75
|
+
api/
|
|
76
|
+
routes/ # Route definitions (thin controllers)
|
|
77
|
+
services/ # Business logic
|
|
78
|
+
models/ # Database models
|
|
79
|
+
schemas/ # Request/response schemas (Pydantic/Zod)
|
|
80
|
+
middleware/ # Auth, rate limiting, CORS
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Anti-Patterns to Avoid
|
|
84
|
+
- Verbs in URIs: `/getUser` → use `GET /users/{id}`
|
|
85
|
+
- Returning 200 for errors → use proper 4xx/5xx
|
|
86
|
+
- Missing Location header on 201
|
|
87
|
+
- Exposing database internals in API responses
|
|
88
|
+
- No input validation
|
|
89
|
+
- Hardcoded credentials
|
|
90
|
+
- Missing CORS configuration
|
|
91
|
+
- No pagination on collection endpoints
|
|
92
|
+
- GET for write operations
|
|
93
|
+
- Not using dependency injection for DB connections
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Python Conventions
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
- Use `src/` layout for libraries, flat layout for apps
|
|
5
|
+
- Package dirs: lowercase with underscores (`my_package`)
|
|
6
|
+
- Always include `__init__.py` in every package dir
|
|
7
|
+
- Include `py.typed` marker for PEP 561
|
|
8
|
+
- Config in `pyproject.toml` (never `setup.py` for new projects)
|
|
9
|
+
- Build backend: `hatchling` or `uv_build`
|
|
10
|
+
- Package manager: `uv` recommended, `poetry` acceptable
|
|
11
|
+
|
|
12
|
+
## Code Style (PEP 8)
|
|
13
|
+
- 4 spaces indentation, no tabs
|
|
14
|
+
- 88 char line length (Black/ruff default)
|
|
15
|
+
- 2 blank lines before top-level functions/classes
|
|
16
|
+
- 1 blank line between methods inside classes
|
|
17
|
+
- snake_case for functions/variables, PascalCase for classes, UPPER_CASE for constants
|
|
18
|
+
|
|
19
|
+
## Imports
|
|
20
|
+
- Order: stdlib → third-party → local (separate with blank lines)
|
|
21
|
+
- One import per line
|
|
22
|
+
- Prefer absolute imports over relative
|
|
23
|
+
- Never use `from x import *`
|
|
24
|
+
|
|
25
|
+
## Type Hints (Modern Python 3.10+)
|
|
26
|
+
- Use built-in generics: `list[int]` not `List[int]`, `dict[str, Any]` not `Dict`
|
|
27
|
+
- Use `X | None` not `Optional[X]`
|
|
28
|
+
- Use `X | Y` not `Union[X, Y]`
|
|
29
|
+
- Always annotate return types and parameters
|
|
30
|
+
- Use `type` keyword for type aliases (Python 3.12+)
|
|
31
|
+
|
|
32
|
+
## Patterns
|
|
33
|
+
- Use `pathlib.Path` over `os.path`
|
|
34
|
+
- Use f-strings over `.format()` or `%` formatting
|
|
35
|
+
- Use `with` context managers for file I/O (always specify `encoding="utf-8"`)
|
|
36
|
+
- Use `dataclasses` over manual `__init__`/`__repr__`/`__eq__`
|
|
37
|
+
- Use `match/case` (Python 3.10+) over long if/elif chains
|
|
38
|
+
- Use `@contextmanager` for custom context managers
|
|
39
|
+
- Check `is None` / `is not None`, never `== None`
|
|
40
|
+
- Check `if x:` not `if len(x) > 0:`
|
|
41
|
+
|
|
42
|
+
## Testing (pytest)
|
|
43
|
+
- Test files: `test_*.py`
|
|
44
|
+
- Test functions: `test_<description>`
|
|
45
|
+
- Fixtures in `conftest.py`
|
|
46
|
+
- Use `@pytest.mark.parametrize` for multiple test cases
|
|
47
|
+
- Use `tmp_path` fixture for file operations
|
|
48
|
+
- Use `monkeypatch` for mocking
|
|
49
|
+
|
|
50
|
+
## Anti-Patterns to Avoid
|
|
51
|
+
- Bare `except:` → use `except Exception:`
|
|
52
|
+
- Mutable default args `def f(x=[])` → use `None` + create inside
|
|
53
|
+
- `os.path` → use `pathlib.Path`
|
|
54
|
+
- `List[int]` → use `list[int]`
|
|
55
|
+
- `print()` in libraries → use `logging`
|
|
56
|
+
- `os.system()` → use `subprocess`
|
|
57
|
+
- `eval()`/`exec()` → use `ast.literal_eval()` if needed
|
|
58
|
+
- Missing `encoding=` on `open()` → always specify `encoding="utf-8"`
|
|
59
|
+
- String concatenation in loops → use `"".join()`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# React / Next.js Conventions
|
|
2
|
+
|
|
3
|
+
## Project Structure (App Router — always prefer over Pages Router)
|
|
4
|
+
```
|
|
5
|
+
src/
|
|
6
|
+
app/
|
|
7
|
+
layout.tsx # Root layout (REQUIRED, must have <html> and <body>)
|
|
8
|
+
page.tsx # Home page
|
|
9
|
+
loading.tsx # Suspense fallback
|
|
10
|
+
error.tsx # Error boundary (MUST be Client Component)
|
|
11
|
+
not-found.tsx # 404 UI
|
|
12
|
+
(route-group)/ # Organize without affecting URL
|
|
13
|
+
[param]/ # Dynamic segments
|
|
14
|
+
api/route.ts # API routes (.ts NOT .tsx)
|
|
15
|
+
components/
|
|
16
|
+
ui/ # Low-level primitives
|
|
17
|
+
lib/ # Utilities, helpers
|
|
18
|
+
hooks/ # Shared custom hooks
|
|
19
|
+
public/ # Static assets
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Component Rules
|
|
23
|
+
- Functional components ONLY — never class components
|
|
24
|
+
- Server Components by default (no directive needed)
|
|
25
|
+
- Add `'use client'` only to leaf interactive components (NOT layouts/pages)
|
|
26
|
+
- Server Components can be `async`, use `await` directly
|
|
27
|
+
- Client Components need `'use client'` at top, can use hooks/events
|
|
28
|
+
|
|
29
|
+
## File Naming
|
|
30
|
+
- Components: PascalCase (`Button.tsx`)
|
|
31
|
+
- Pages/Routes: lowercase (`page.tsx`, `layout.tsx`)
|
|
32
|
+
- Hooks: camelCase with `use` prefix (`useMediaQuery.ts`)
|
|
33
|
+
- Utilities: camelCase (`formatDate.ts`)
|
|
34
|
+
- API routes: `route.ts` (NOT .tsx)
|
|
35
|
+
|
|
36
|
+
## Import Order
|
|
37
|
+
1. React/Next.js core (`react`, `next/navigation`, `next/link`)
|
|
38
|
+
2. Third-party libraries
|
|
39
|
+
3. Internal components (`@/components/...`)
|
|
40
|
+
4. Internal hooks/utils (`@/hooks/...`, `@/lib/...`)
|
|
41
|
+
5. Types (`import type { ... }`)
|
|
42
|
+
|
|
43
|
+
Use `@/` path alias for all internal imports.
|
|
44
|
+
|
|
45
|
+
## State Management
|
|
46
|
+
- Local state: `useState` for simple, `useReducer` for complex
|
|
47
|
+
- Global state: Context (sparingly) or Zustand
|
|
48
|
+
- Server state: React Query / TanStack Query
|
|
49
|
+
- Lift state up to closest common parent when shared
|
|
50
|
+
|
|
51
|
+
## Styling
|
|
52
|
+
- Tailwind CSS as default
|
|
53
|
+
- Use `cn()` utility (clsx + tailwind-merge) for conditional classes
|
|
54
|
+
- Use `className` not `class`
|
|
55
|
+
- Avoid inline styles except for dynamic values
|
|
56
|
+
|
|
57
|
+
## TypeScript
|
|
58
|
+
- Never use `any` — use `unknown` and narrow
|
|
59
|
+
- Use `React.ReactNode` for children props
|
|
60
|
+
- Use type imports: `import type { User } from '@/types'`
|
|
61
|
+
- Use Zod for runtime validation
|
|
62
|
+
|
|
63
|
+
## Data Fetching
|
|
64
|
+
- Server Components: direct `await fetch()` or DB queries
|
|
65
|
+
- Client Components: React Query or SWR
|
|
66
|
+
- Never use `useEffect` for data fetching in Server Components
|
|
67
|
+
- Parallel fetching: `Promise.all()`
|
|
68
|
+
|
|
69
|
+
## Performance
|
|
70
|
+
- Don't overuse `memo`, `useMemo`, `useCallback` — only when measured
|
|
71
|
+
- Lazy load with `dynamic()` for heavy components
|
|
72
|
+
- Use `loading.tsx` for instant loading states
|
|
73
|
+
|
|
74
|
+
## Anti-Patterns to Avoid
|
|
75
|
+
- Class components → functional only
|
|
76
|
+
- `'use client'` on root layout → only on leaf components
|
|
77
|
+
- `useEffect` for data fetching → Server Components or React Query
|
|
78
|
+
- `class` attribute → use `className`
|
|
79
|
+
- Barrel exports (`index.ts`) → import directly from files
|
|
80
|
+
- `.tsx` for API routes → use `.ts`
|
|
81
|
+
- Passing non-serializable props Server→Client
|
|
82
|
+
- Missing `<html>` and `<body>` in root layout
|
|
83
|
+
- Using Pages Router for new projects
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Web Development Conventions
|
|
2
|
+
|
|
3
|
+
## File Structure
|
|
4
|
+
- Single-file apps: `< 300 LOC` → single `index.html` with inline `<style>` and `<script>`
|
|
5
|
+
- Multi-file apps: `index.html` + `css/` + `js/` + `assets/`
|
|
6
|
+
- Files: lowercase-kebab-case (`my-component.js`)
|
|
7
|
+
- CSS classes: BEM or kebab-case (`.card__title--large`)
|
|
8
|
+
- JS variables/functions: camelCase
|
|
9
|
+
|
|
10
|
+
## HTML Rules
|
|
11
|
+
- Always include: `<!DOCTYPE html>`, `<html lang="en">`, `<meta charset="UTF-8">`, viewport meta
|
|
12
|
+
- Use semantic elements: `<header>`, `<nav>`, `<main>`, `<article>`, `<section>`, `<footer>`
|
|
13
|
+
- Every `<img>` must have `alt` attribute
|
|
14
|
+
- Use `<button>` for actions, `<a>` for navigation (not `<div onclick>`)
|
|
15
|
+
- Maintain heading hierarchy: h1 > h2 > h3 (never skip)
|
|
16
|
+
- Add `rel="noopener noreferrer"` on `target="_blank"` links
|
|
17
|
+
|
|
18
|
+
## CSS Rules
|
|
19
|
+
- Use CSS custom properties for repeated values
|
|
20
|
+
- Use `box-sizing: border-box` universally
|
|
21
|
+
- Use `rem` for spacing/sizing, `px` only for borders
|
|
22
|
+
- Mobile-first responsive design with `min-width` media queries
|
|
23
|
+
- Flexbox for 1D layouts, Grid for 2D layouts
|
|
24
|
+
- Use `gap` property over margin hacks
|
|
25
|
+
- Never use `float` for layout, never use `!important`
|
|
26
|
+
- Max selector depth: 3 levels
|
|
27
|
+
- Use `clamp()` for fluid typography
|
|
28
|
+
|
|
29
|
+
## JavaScript Rules
|
|
30
|
+
- Always use `const` by default, `let` when reassignment needed, never `var`
|
|
31
|
+
- Always use `addEventListener`, never inline handlers
|
|
32
|
+
- Use `textContent` for user-provided text (XSS prevention), never `innerHTML` with unsanitized input
|
|
33
|
+
- Always use `try/catch` with async operations
|
|
34
|
+
- Use `===` not `==`
|
|
35
|
+
- Use arrow functions for callbacks
|
|
36
|
+
- Use template literals over string concatenation
|
|
37
|
+
- Use optional chaining `?.` and nullish coalescing `??`
|
|
38
|
+
- Never use `eval()`
|
|
39
|
+
|
|
40
|
+
## Security
|
|
41
|
+
- Never insert user input as HTML (use `textContent`)
|
|
42
|
+
- Validate client-side AND server-side
|
|
43
|
+
- Never store secrets in localStorage
|
|
44
|
+
- Use HTTPS for all external resources
|
|
45
|
+
- Don't expose API keys in client-side code
|
|
46
|
+
|
|
47
|
+
## Anti-Patterns to Avoid
|
|
48
|
+
- `<div onclick>` → use `<button>` or `addEventListener`
|
|
49
|
+
- `float: left` for grid → use flexbox or grid
|
|
50
|
+
- `var` → use `const`/`let`
|
|
51
|
+
- `==` → use `===`
|
|
52
|
+
- `innerHTML = userInput` → use `textContent`
|
|
53
|
+
- Global variables → use modules or block scope
|
|
54
|
+
- `.then()` chains → use `async/await`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Skill loader — auto-detects project type and loads relevant conventions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import functools
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SKILLS_DIR = Path(__file__).parent / "bundled"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@functools.lru_cache(maxsize=1)
|
|
12
|
+
def _detect_project_type() -> list[str]:
|
|
13
|
+
cwd = Path.cwd()
|
|
14
|
+
detected = []
|
|
15
|
+
|
|
16
|
+
indicators = {
|
|
17
|
+
"web-dev": {
|
|
18
|
+
"files": ["index.html", "*.html"],
|
|
19
|
+
"extensions": [".html", ".htm", ".css", ".scss", ".less"],
|
|
20
|
+
"keywords": ["html", "css", "javascript", "website", "webpage", "landing page"],
|
|
21
|
+
},
|
|
22
|
+
"python": {
|
|
23
|
+
"files": ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile", "uv.lock"],
|
|
24
|
+
"extensions": [".py", ".pyi", ".ipynb"],
|
|
25
|
+
"keywords": ["python", "pip", "uv", "poetry", "pytest", "django", "flask", "fastapi"],
|
|
26
|
+
},
|
|
27
|
+
"react": {
|
|
28
|
+
"files": ["next.config.ts", "next.config.js", "next.config.mjs"],
|
|
29
|
+
"extensions": [".tsx", ".jsx"],
|
|
30
|
+
"keywords": ["react", "next.js", "nextjs", "remix", "vite"],
|
|
31
|
+
"package_deps": ["react", "next"],
|
|
32
|
+
},
|
|
33
|
+
"api-design": {
|
|
34
|
+
"files": ["routes.py", "models.py", "schemas.py"],
|
|
35
|
+
"extensions": [],
|
|
36
|
+
"keywords": ["api", "endpoint", "rest", "graphql", "server", "backend", "fastapi", "express", "flask"],
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
html_files = list(cwd.rglob("*.html"))
|
|
41
|
+
py_files = list(cwd.rglob("*.py"))
|
|
42
|
+
tsx_files = list(cwd.rglob("*.tsx")) + list(cwd.rglob("*.jsx"))
|
|
43
|
+
|
|
44
|
+
for skill_name, config in indicators.items():
|
|
45
|
+
score = 0
|
|
46
|
+
|
|
47
|
+
for pattern in config["files"]:
|
|
48
|
+
if list(cwd.glob(pattern)):
|
|
49
|
+
score += 3
|
|
50
|
+
|
|
51
|
+
for ext in config["extensions"]:
|
|
52
|
+
if skill_name == "web-dev" and ext == ".html" and len(html_files) > 0:
|
|
53
|
+
score += min(len(html_files), 3)
|
|
54
|
+
elif skill_name == "python" and ext == ".py" and len(py_files) > 0:
|
|
55
|
+
score += min(len(py_files), 3)
|
|
56
|
+
elif skill_name in ("react",) and ext in (".tsx", ".jsx") and len(tsx_files) > 0:
|
|
57
|
+
score += min(len(tsx_files), 3)
|
|
58
|
+
|
|
59
|
+
if skill_name == "react":
|
|
60
|
+
pkg_json = cwd / "package.json"
|
|
61
|
+
if pkg_json.exists():
|
|
62
|
+
try:
|
|
63
|
+
pkg = json.loads(pkg_json.read_text())
|
|
64
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
65
|
+
for dep in config.get("package_deps", []):
|
|
66
|
+
if dep in deps:
|
|
67
|
+
score += 5
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
if skill_name == "python":
|
|
72
|
+
if (cwd / "pyproject.toml").exists():
|
|
73
|
+
score += 5
|
|
74
|
+
if any(cwd.rglob("fastapi")) or any((cwd / f).exists() for f in ["main.py", "app.py"]):
|
|
75
|
+
score += 2
|
|
76
|
+
|
|
77
|
+
if score >= 2:
|
|
78
|
+
detected.append((skill_name, score))
|
|
79
|
+
|
|
80
|
+
detected.sort(key=lambda x: x[1], reverse=True)
|
|
81
|
+
return [name for name, _ in detected]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_skill(skill_name: str) -> str | None:
|
|
85
|
+
skill_dir = SKILLS_DIR / skill_name
|
|
86
|
+
if not skill_dir.exists():
|
|
87
|
+
return None
|
|
88
|
+
conventions_file = skill_dir / "conventions.md"
|
|
89
|
+
if conventions_file.exists():
|
|
90
|
+
return conventions_file.read_text(encoding="utf-8")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_relevant_skills() -> list[str]:
|
|
95
|
+
return _detect_project_type()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_skill_context() -> str:
|
|
99
|
+
skills = get_relevant_skills()
|
|
100
|
+
if not skills:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
parts = []
|
|
104
|
+
for skill_name in skills:
|
|
105
|
+
content = load_skill(skill_name)
|
|
106
|
+
if content:
|
|
107
|
+
parts.append(f"## {skill_name} conventions\n{content}")
|
|
108
|
+
|
|
109
|
+
return "\n\n".join(parts)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
|
|
6
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
7
|
+
warnings.filterwarnings("ignore", module="huggingface_hub")
|
|
8
|
+
warnings.filterwarnings("ignore", module="sentence_transformers")
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
|
13
|
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
|
14
|
+
CHROMA_PERSIST_DIR = os.getenv("CHROMA_PERSIST_DIR", ".librarian/memory")
|
|
15
|
+
EMBED_MODEL = os.getenv("EMBED_MODEL", "all-MiniLM-L6-v2")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.theme import Theme
|
|
3
|
+
|
|
4
|
+
theme = Theme({
|
|
5
|
+
"info": "bold #6366F1",
|
|
6
|
+
"success": "bold #10B981",
|
|
7
|
+
"warning": "bold #F59E0B",
|
|
8
|
+
"error": "bold #EF4444",
|
|
9
|
+
"muted": "#6B7280",
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
console = Console(theme=theme)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def log_info(msg):
|
|
16
|
+
console.print(f"[info]→[/info] {msg}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def log_success(msg):
|
|
20
|
+
console.print(f"[success]✓[/success] {msg}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def log_warning(msg):
|
|
24
|
+
console.print(f"[warning]![/warning] {msg}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def log_error(msg):
|
|
28
|
+
console.print(f"[error]✗[/error] {msg}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def log_muted(msg):
|
|
32
|
+
console.print(f"[muted]{msg}[/muted]")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class TokenTracker:
|
|
2
|
+
def __init__(self):
|
|
3
|
+
self.session_tokens = {"groq": 0, "openrouter": 0}
|
|
4
|
+
|
|
5
|
+
def add(self, provider: str, tokens: int):
|
|
6
|
+
if provider in self.session_tokens:
|
|
7
|
+
self.session_tokens[provider] += tokens
|
|
8
|
+
|
|
9
|
+
def total(self) -> int:
|
|
10
|
+
return sum(self.session_tokens.values())
|
|
11
|
+
|
|
12
|
+
def report(self) -> str:
|
|
13
|
+
return f"groq: {self.session_tokens['groq']} · openrouter: {self.session_tokens['openrouter']}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
tracker = TokenTracker()
|