ragtime-cli 0.2.3__py3-none-any.whl → 0.2.4__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.

Potentially problematic release.


This version of ragtime-cli might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragtime-cli
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Local-first memory and RAG system for Claude Code - semantic search over code, docs, and team knowledge
5
5
  Author-email: Bret Martineau <bretwardjames@gmail.com>
6
6
  License-Expression: MIT
@@ -37,8 +37,12 @@ Local-first memory and RAG system for Claude Code. Semantic search over code, do
37
37
  - **Memory Storage**: Store structured knowledge with namespaces, types, and metadata
38
38
  - **Semantic Search**: Query memories and docs with natural language
39
39
  - **Cross-Branch Sync**: Share context with teammates before PRs merge
40
+ - **Convention Checking**: Verify code follows team standards before PRs
41
+ - **Doc Generation**: Generate documentation from code (stubs or AI-powered)
42
+ - **Debug Tools**: Verify index integrity, inspect similarity scores
40
43
  - **MCP Server**: Native Claude Code integration
41
- - **Claude Commands**: Pre-built `/remember`, `/recall`, `/handoff`, `/start` commands
44
+ - **Claude Commands**: Pre-built `/remember`, `/recall`, `/create-pr`, `/generate-docs` commands
45
+ - **ghp-cli Integration**: Auto-context when starting issues
42
46
 
43
47
  ## Installation
44
48
 
@@ -52,6 +56,9 @@ pip install ragtime-cli
52
56
  # Initialize in your project
53
57
  ragtime init
54
58
 
59
+ # Index your docs
60
+ ragtime index
61
+
55
62
  # Store a memory
56
63
  ragtime remember "Auth uses JWT with 15-min expiry" \
57
64
  --namespace app \
@@ -63,6 +70,9 @@ ragtime search "authentication" --namespace app
63
70
 
64
71
  # Install Claude commands
65
72
  ragtime install --workspace
73
+
74
+ # Check for updates
75
+ ragtime update --check
66
76
  ```
67
77
 
68
78
  ## CLI Commands
@@ -94,19 +104,75 @@ ragtime search "how does auth work" --namespace app --limit 10
94
104
 
95
105
  # Reindex memory files
96
106
  ragtime reindex
107
+
108
+ # Audit docs for missing frontmatter
109
+ ragtime audit docs/
110
+ ragtime audit docs/ --fix # Interactively add frontmatter
111
+ ragtime audit docs/ --json # Machine-readable output
112
+ ```
113
+
114
+ ### Documentation Generation
115
+
116
+ ```bash
117
+ # Generate doc stubs from code
118
+ ragtime generate src/ --stubs
119
+
120
+ # Specify output location
121
+ ragtime generate src/ --stubs -o docs/api
122
+
123
+ # Python only
124
+ ragtime generate src/ --stubs -l python
125
+
126
+ # Include private methods
127
+ ragtime generate src/ --stubs --include-private
128
+ ```
129
+
130
+ ### Debug & Verification
131
+
132
+ ```bash
133
+ # Debug a search query (show similarity scores)
134
+ ragtime debug search "authentication"
135
+ ragtime debug search "auth" --show-vectors
136
+
137
+ # Find similar documents
138
+ ragtime debug similar docs/auth/jwt.md
139
+
140
+ # Index statistics by namespace/type
141
+ ragtime debug stats
142
+ ragtime debug stats --by-namespace
143
+ ragtime debug stats --by-type
144
+
145
+ # Verify index integrity
146
+ ragtime debug verify
97
147
  ```
98
148
 
99
149
  ### Cross-Branch Sync
100
150
 
101
151
  ```bash
102
- # Sync teammate's branch memories
103
- ragtime sync origin/jm/feature-auth
152
+ # Sync all teammate branch memories
153
+ ragtime sync
104
154
 
105
- # Clean up stale synced folders
155
+ # Auto-prune stale synced folders
156
+ ragtime sync --auto-prune
157
+
158
+ # Manual prune
106
159
  ragtime prune --dry-run
107
160
  ragtime prune
108
161
  ```
109
162
 
163
+ ### Daemon (Auto-Sync)
164
+
165
+ ```bash
166
+ # Start background sync daemon
167
+ ragtime daemon start --interval 5m
168
+
169
+ # Check status
170
+ ragtime daemon status
171
+
172
+ # Stop daemon
173
+ ragtime daemon stop
174
+ ```
175
+
110
176
  ### Claude Integration
111
177
 
112
178
  ```bash
@@ -118,48 +184,47 @@ ragtime install --global
118
184
 
119
185
  # List available commands
120
186
  ragtime install --list
121
- ```
122
187
 
123
- ## MCP Server
124
-
125
- Add to your Claude config (`.mcp.json`):
126
-
127
- ```json
128
- {
129
- "mcpServers": {
130
- "ragtime": {
131
- "command": "ragtime-mcp",
132
- "args": ["--path", "."]
133
- }
134
- }
135
- }
188
+ # Set up ghp-cli hooks
189
+ ragtime setup-ghp
136
190
  ```
137
191
 
138
- Available tools:
139
- - `remember` - Store a memory
140
- - `search` - Semantic search
141
- - `list_memories` - List with filters
142
- - `get_memory` - Get by ID
143
- - `store_doc` - Store document verbatim
144
- - `forget` - Delete memory
145
- - `graduate` - Promote branch → app
146
- - `update_status` - Change memory status
147
-
148
192
  ## Storage Structure
149
193
 
150
194
  ```
151
- .claude/memory/
152
- ├── app/{component}/ # Graduated app knowledge
195
+ .ragtime/
196
+ ├── config.yaml # Configuration
197
+ ├── CONVENTIONS.md # Team rules (checked by /create-pr)
198
+ ├── app/{component}/ # Graduated app knowledge (tracked)
153
199
  │ └── {id}-{slug}.md
154
- ├── team/ # Team conventions
200
+ ├── team/ # Team conventions (tracked)
155
201
  │ └── {id}-{slug}.md
156
- ├── users/{username}/ # User preferences
157
- └── {id}-{slug}.md
158
- └── branches/
159
- ├── {branch-slug}/ # Your branch (tracked in git)
160
- ├── context.md # Session handoff
161
- │ └── {id}-{slug}.md
162
- └── {branch}(unmerged)/ # Synced from teammates (gitignored)
202
+ ├── branches/
203
+ ├── {branch-slug}/ # Your branch (tracked in git)
204
+ │ │ ├── context.md
205
+ │ │ └── {id}-{slug}.md
206
+ └── .{branch-slug}/ # Synced from teammates (gitignored, dot-prefix)
207
+ ├── archive/branches/ # Archived completed branches (tracked)
208
+ └── index/ # ChromaDB vector store (gitignored)
209
+ ```
210
+
211
+ ## Configuration
212
+
213
+ `.ragtime/config.yaml`:
214
+
215
+ ```yaml
216
+ docs:
217
+ paths: ["docs", ".ragtime"]
218
+ patterns: ["**/*.md"]
219
+ exclude: ["**/node_modules/**"]
220
+
221
+ code:
222
+ paths: ["."]
223
+ languages: ["python", "typescript", "dart"]
224
+
225
+ conventions:
226
+ files: [".ragtime/CONVENTIONS.md"]
227
+ also_search_memories: true
163
228
  ```
164
229
 
165
230
  ## Memory Format
@@ -174,7 +239,7 @@ type: architecture
174
239
  component: auth
175
240
  confidence: high
176
241
  status: active
177
- added: '2026-01-31'
242
+ added: '2025-01-31'
178
243
  author: bretwardjames
179
244
  ---
180
245
 
@@ -200,6 +265,7 @@ Sessions are stored in Redis, not cookies.
200
265
  | `decision` | Why we chose X over Y |
201
266
  | `convention` | Team standards |
202
267
  | `pattern` | Reusable approaches |
268
+ | `integration` | External service connections |
203
269
  | `context` | Session handoff |
204
270
 
205
271
  ## Claude Commands
@@ -212,8 +278,79 @@ After `ragtime install --workspace`:
212
278
  | `/recall` | Search memories |
213
279
  | `/handoff` | Save session context |
214
280
  | `/start` | Resume work on an issue |
215
- | `/pr-graduate` | Curate branch knowledge after merge |
216
- | `/audit` | Find duplicates/conflicts |
281
+ | `/create-pr` | Check conventions, graduate memories, create PR |
282
+ | `/generate-docs` | AI-powered documentation generation from code |
283
+ | `/import-docs` | Migrate existing docs to memories |
284
+ | `/pr-graduate` | Curate branch knowledge (fallback if forgot before PR) |
285
+ | `/audit` | Find duplicates/conflicts in memories |
286
+
287
+ ## MCP Server
288
+
289
+ Add to your Claude config (`.mcp.json`):
290
+
291
+ ```json
292
+ {
293
+ "mcpServers": {
294
+ "ragtime": {
295
+ "command": "ragtime-mcp",
296
+ "args": ["--path", "."]
297
+ }
298
+ }
299
+ }
300
+ ```
301
+
302
+ Available tools:
303
+ - `remember` - Store a memory
304
+ - `search` - Semantic search
305
+ - `list_memories` - List with filters
306
+ - `get_memory` - Get by ID
307
+ - `store_doc` - Store document verbatim
308
+ - `forget` - Delete memory
309
+ - `graduate` - Promote branch → app
310
+ - `update_status` - Change memory status
311
+
312
+ ## ghp-cli Integration
313
+
314
+ If you use [ghp-cli](https://github.com/bretwardjames/ghp-cli):
315
+
316
+ ```bash
317
+ # Register ragtime hooks
318
+ ragtime setup-ghp
319
+ ```
320
+
321
+ This auto-creates `context.md` from issue details when you run `ghp start`.
322
+
323
+ ## Workflow
324
+
325
+ ### Starting Work
326
+
327
+ ```bash
328
+ ghp start 123 # Creates branch + context.md
329
+ # or
330
+ ragtime new-branch 123 # Just the context
331
+ ```
332
+
333
+ ### During Development
334
+
335
+ ```bash
336
+ /remember "API uses rate limiting" # Capture insights
337
+ /handoff # Save progress for later
338
+ ```
339
+
340
+ ### Before PR
341
+
342
+ ```bash
343
+ /create-pr
344
+ # 1. Checks code against CONVENTIONS.md
345
+ # 2. Reviews branch memories
346
+ # 3. Graduates selected memories to app/
347
+ # 4. Commits knowledge with code
348
+ # 5. Creates PR
349
+ ```
350
+
351
+ ### After Merge
352
+
353
+ Graduated knowledge is already in the PR. Run `ragtime prune` to clean up synced folders.
217
354
 
218
355
  ## License
219
356
 
@@ -1,12 +1,13 @@
1
- ragtime_cli-0.2.3.dist-info/licenses/LICENSE,sha256=9A0wJs2PRDciGRH4F8JUJ-aMKYQyq_gVu2ixrXs-l5A,1070
1
+ ragtime_cli-0.2.4.dist-info/licenses/LICENSE,sha256=9A0wJs2PRDciGRH4F8JUJ-aMKYQyq_gVu2ixrXs-l5A,1070
2
2
  src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- src/cli.py,sha256=PZ6NHl7rOyUVZ2HAcGPoytNRtsHczybTBy7A6xQpgoA,47320
3
+ src/cli.py,sha256=iNCYKD6w1gi64eNeb8BFj-xPzVnQLqHMzmPzoRGE3tI,67135
4
4
  src/config.py,sha256=kaJV-7yI-LMW16e1w6JvM1ZCjgPf8EiBBOM4OXio6I8,4045
5
5
  src/db.py,sha256=BKrlhilXYHNaj-ZcffinSXVhdUqowmwpFPBx7aLxamU,4642
6
6
  src/mcp_server.py,sha256=Tx0i73GXO0YmcVrdO7UjRMS0auN8fBG2LOpHuf_LUC0,20374
7
7
  src/memory.py,sha256=POT2lYeBcEx4_MPbsIdet6ScwcjmuETz8Dxmz-Z_7IY,11939
8
8
  src/commands/audit.md,sha256=Xkucm-gfBIMalK9wf7NBbyejpsqBTUAGGlb7GxMtMPY,5137
9
9
  src/commands/create-pr.md,sha256=u6-jVkDP_6bJQp6ImK039eY9F6B9E2KlAVlvLY-WV6Q,9483
10
+ src/commands/generate-docs.md,sha256=9W2Yy-PDyC3p5k39uEb31z5YAHkSKsQLg6gV3tLgSnQ,7015
10
11
  src/commands/handoff.md,sha256=8VxTddtW08jGTW36GW_rS77JdeSn8vHeMfklrWwVUD4,5055
11
12
  src/commands/import-docs.md,sha256=ByIdcfbdiF77HoFv5U6zZ_YvZf00-hAs9EMconXssvY,6927
12
13
  src/commands/pr-graduate.md,sha256=nXJMuXeOp0SZfjQ567NUO02Rg9zPQHQFbZbJ4Q_His0,6692
@@ -16,8 +17,8 @@ src/commands/save.md,sha256=7gTpW46AU9Y4l8XVZ8f4h1sEdBfVqIRA7hlidUxMAC4,251
16
17
  src/commands/start.md,sha256=qoqhkMgET74DBx8YPIT1-wqCiVBUDxlmevigsCinHSY,6506
17
18
  src/indexers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  src/indexers/docs.py,sha256=7FENHaKSvC1T557bRzvmrjyaG_vK94GuQG9XMZdr89w,3349
19
- ragtime_cli-0.2.3.dist-info/METADATA,sha256=QaKUlKE4A2BwGX3NNTXm02JLwko_msFmJg3K6RyOmmo,5311
20
- ragtime_cli-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
- ragtime_cli-0.2.3.dist-info/entry_points.txt,sha256=cWLbeyMxZNbew-THS3bHXTpCRXt1EaUy5QUOXGXLjl4,75
22
- ragtime_cli-0.2.3.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
23
- ragtime_cli-0.2.3.dist-info/RECORD,,
20
+ ragtime_cli-0.2.4.dist-info/METADATA,sha256=VtuTNejyKDZqNrVuC5tJGNHWDHVrIY-2Pk1-k0NlwWY,8383
21
+ ragtime_cli-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ ragtime_cli-0.2.4.dist-info/entry_points.txt,sha256=cWLbeyMxZNbew-THS3bHXTpCRXt1EaUy5QUOXGXLjl4,75
23
+ ragtime_cli-0.2.4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
24
+ ragtime_cli-0.2.4.dist-info/RECORD,,
src/cli.py CHANGED
@@ -166,7 +166,7 @@ def get_remote_branches_with_ragtime(path: Path) -> list[str]:
166
166
 
167
167
 
168
168
  @click.group()
169
- @click.version_option(version="0.2.3")
169
+ @click.version_option(version="0.2.4")
170
170
  def main():
171
171
  """Ragtime - semantic search over code and documentation."""
172
172
  pass
@@ -1175,6 +1175,586 @@ def daemon_status(path: Path):
1175
1175
  pid_file.unlink()
1176
1176
 
1177
1177
 
1178
+ # ============================================================================
1179
+ # Debug Commands
1180
+ # ============================================================================
1181
+
1182
+
1183
+ @main.group()
1184
+ def debug():
1185
+ """Debug and verify the vector index."""
1186
+ pass
1187
+
1188
+
1189
+ @debug.command("search")
1190
+ @click.argument("query")
1191
+ @click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
1192
+ @click.option("--limit", "-l", default=5, help="Max results")
1193
+ @click.option("--show-vectors", is_flag=True, help="Show vector statistics")
1194
+ def debug_search(query: str, path: Path, limit: int, show_vectors: bool):
1195
+ """Debug a search query - show scores and ranking details."""
1196
+ path = Path(path).resolve()
1197
+ db = get_db(path)
1198
+
1199
+ results = db.search(query=query, limit=limit)
1200
+
1201
+ if not results:
1202
+ click.echo("No results found.")
1203
+ return
1204
+
1205
+ click.echo(f"\nQuery: \"{query}\"")
1206
+ click.echo(f"{'─' * 60}")
1207
+
1208
+ for i, result in enumerate(results, 1):
1209
+ meta = result["metadata"]
1210
+ distance = result["distance"]
1211
+ similarity = 1 - distance if distance else None
1212
+
1213
+ click.echo(f"\n[{i}] {meta.get('file', 'unknown')}")
1214
+ click.echo(f" Distance: {distance:.4f}")
1215
+ click.echo(f" Similarity: {similarity:.4f} ({similarity * 100:.1f}%)")
1216
+ click.echo(f" Namespace: {meta.get('namespace', '-')}")
1217
+ click.echo(f" Type: {meta.get('type', '-')}")
1218
+
1219
+ # Show content preview
1220
+ preview = result["content"][:100].replace("\n", " ")
1221
+ click.echo(f" Preview: {preview}...")
1222
+
1223
+ if show_vectors:
1224
+ click.echo(f"\n{'─' * 60}")
1225
+ click.echo("Vector Statistics:")
1226
+ click.echo(f" Total indexed: {db.collection.count()}")
1227
+ click.echo(f" Embedding model: all-MiniLM-L6-v2 (ChromaDB default)")
1228
+ click.echo(f" Vector dimensions: 384")
1229
+ click.echo(f" Distance metric: cosine")
1230
+
1231
+
1232
+ @debug.command("similar")
1233
+ @click.argument("file_path", type=click.Path(exists=True))
1234
+ @click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
1235
+ @click.option("--limit", "-l", default=5, help="Max similar docs")
1236
+ def debug_similar(file_path: str, path: Path, limit: int):
1237
+ """Find documents similar to a given file."""
1238
+ path = Path(path).resolve()
1239
+ db = get_db(path)
1240
+
1241
+ # Read the file content
1242
+ try:
1243
+ content = Path(file_path).read_text()
1244
+ except Exception as e:
1245
+ click.echo(f"✗ Could not read file: {e}", err=True)
1246
+ return
1247
+
1248
+ # Use the content as the query
1249
+ results = db.search(query=content, limit=limit + 1) # +1 to exclude self
1250
+
1251
+ click.echo(f"\nDocuments similar to: {file_path}")
1252
+ click.echo(f"{'─' * 60}")
1253
+
1254
+ shown = 0
1255
+ for result in results:
1256
+ # Skip the file itself
1257
+ if result["metadata"].get("file", "").endswith(file_path):
1258
+ continue
1259
+
1260
+ shown += 1
1261
+ if shown > limit:
1262
+ break
1263
+
1264
+ meta = result["metadata"]
1265
+ distance = result["distance"]
1266
+ similarity = 1 - distance if distance else None
1267
+
1268
+ click.echo(f"\n[{shown}] {meta.get('file', 'unknown')}")
1269
+ click.echo(f" Similarity: {similarity:.4f} ({similarity * 100:.1f}%)")
1270
+
1271
+ preview = result["content"][:100].replace("\n", " ")
1272
+ click.echo(f" Preview: {preview}...")
1273
+
1274
+
1275
+ @debug.command("stats")
1276
+ @click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
1277
+ @click.option("--by-namespace", is_flag=True, help="Show counts by namespace")
1278
+ @click.option("--by-type", is_flag=True, help="Show counts by type")
1279
+ def debug_stats(path: Path, by_namespace: bool, by_type: bool):
1280
+ """Show detailed index statistics."""
1281
+ path = Path(path).resolve()
1282
+ db = get_db(path)
1283
+
1284
+ total = db.collection.count()
1285
+ click.echo(f"\nIndex Statistics")
1286
+ click.echo(f"{'─' * 40}")
1287
+ click.echo(f"Total documents: {total}")
1288
+
1289
+ if total == 0:
1290
+ click.echo("\nIndex is empty. Run 'ragtime index' first.")
1291
+ return
1292
+
1293
+ # Get all documents for analysis
1294
+ all_docs = db.collection.get()
1295
+
1296
+ if by_namespace or (not by_namespace and not by_type):
1297
+ namespaces = {}
1298
+ for meta in all_docs["metadatas"]:
1299
+ ns = meta.get("namespace", "unknown")
1300
+ namespaces[ns] = namespaces.get(ns, 0) + 1
1301
+
1302
+ click.echo(f"\nBy Namespace:")
1303
+ for ns, count in sorted(namespaces.items(), key=lambda x: -x[1]):
1304
+ pct = count / total * 100
1305
+ click.echo(f" {ns}: {count} ({pct:.1f}%)")
1306
+
1307
+ if by_type or (not by_namespace and not by_type):
1308
+ types = {}
1309
+ for meta in all_docs["metadatas"]:
1310
+ t = meta.get("type", "unknown")
1311
+ types[t] = types.get(t, 0) + 1
1312
+
1313
+ click.echo(f"\nBy Type:")
1314
+ for t, count in sorted(types.items(), key=lambda x: -x[1]):
1315
+ pct = count / total * 100
1316
+ click.echo(f" {t}: {count} ({pct:.1f}%)")
1317
+
1318
+
1319
+ @debug.command("verify")
1320
+ @click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
1321
+ def debug_verify(path: Path):
1322
+ """Verify index integrity with test queries."""
1323
+ path = Path(path).resolve()
1324
+ db = get_db(path)
1325
+
1326
+ total = db.collection.count()
1327
+ if total == 0:
1328
+ click.echo("✗ Index is empty. Run 'ragtime index' first.")
1329
+ return
1330
+
1331
+ click.echo(f"\nVerifying index ({total} documents)...")
1332
+ click.echo(f"{'─' * 40}")
1333
+
1334
+ issues = []
1335
+
1336
+ # Test 1: Basic search works
1337
+ click.echo("\n1. Testing basic search...")
1338
+ try:
1339
+ results = db.search("test", limit=1)
1340
+ if results:
1341
+ click.echo(" ✓ Search returns results")
1342
+ else:
1343
+ click.echo(" ⚠ Search returned no results (might be ok if no relevant docs)")
1344
+ except Exception as e:
1345
+ click.echo(f" ✗ Search failed: {e}")
1346
+ issues.append("Basic search failed")
1347
+
1348
+ # Test 2: Check for documents with missing metadata
1349
+ click.echo("\n2. Checking metadata integrity...")
1350
+ all_docs = db.collection.get()
1351
+ missing_namespace = 0
1352
+ missing_type = 0
1353
+
1354
+ for meta in all_docs["metadatas"]:
1355
+ if not meta.get("namespace"):
1356
+ missing_namespace += 1
1357
+ if not meta.get("type"):
1358
+ missing_type += 1
1359
+
1360
+ if missing_namespace:
1361
+ click.echo(f" ⚠ {missing_namespace} docs missing namespace")
1362
+ else:
1363
+ click.echo(" ✓ All docs have namespace")
1364
+
1365
+ if missing_type:
1366
+ click.echo(f" ⚠ {missing_type} docs missing type")
1367
+ else:
1368
+ click.echo(" ✓ All docs have type")
1369
+
1370
+ # Test 3: Check for duplicate IDs
1371
+ click.echo("\n3. Checking for duplicates...")
1372
+ ids = all_docs["ids"]
1373
+ unique_ids = set(ids)
1374
+ if len(ids) != len(unique_ids):
1375
+ dup_count = len(ids) - len(unique_ids)
1376
+ click.echo(f" ✗ {dup_count} duplicate IDs found")
1377
+ issues.append("Duplicate IDs")
1378
+ else:
1379
+ click.echo(" ✓ No duplicate IDs")
1380
+
1381
+ # Test 4: Similarity sanity check
1382
+ click.echo("\n4. Testing similarity consistency...")
1383
+ if total >= 2:
1384
+ # Pick first doc and find similar
1385
+ first_content = all_docs["documents"][0]
1386
+ results = db.search(first_content[:500], limit=2)
1387
+ if results and len(results) >= 1:
1388
+ top_similarity = 1 - results[0]["distance"]
1389
+ if top_similarity > 0.9:
1390
+ click.echo(f" ✓ Self-similarity check passed ({top_similarity:.2f})")
1391
+ else:
1392
+ click.echo(f" ⚠ Self-similarity lower than expected ({top_similarity:.2f})")
1393
+ else:
1394
+ click.echo(" ⚠ Could not perform similarity check")
1395
+ else:
1396
+ click.echo(" - Skipped (need at least 2 docs)")
1397
+
1398
+ # Summary
1399
+ click.echo(f"\n{'─' * 40}")
1400
+ if issues:
1401
+ click.echo(f"⚠ Found {len(issues)} issues:")
1402
+ for issue in issues:
1403
+ click.echo(f" - {issue}")
1404
+ else:
1405
+ click.echo("✓ Index verification passed")
1406
+
1407
+
1408
+ # ============================================================================
1409
+ # Documentation Generation
1410
+ # ============================================================================
1411
+
1412
+
1413
+ @main.command()
1414
+ @click.argument("code_path", type=click.Path(exists=True, path_type=Path))
1415
+ @click.option("--output", "-o", type=click.Path(path_type=Path), default="docs/api",
1416
+ help="Output directory for docs")
1417
+ @click.option("--stubs", is_flag=True, help="Generate stub docs with TODOs (no AI)")
1418
+ @click.option("--language", "-l", multiple=True,
1419
+ help="Languages to document (python, typescript, javascript)")
1420
+ @click.option("--include-private", is_flag=True, help="Include private methods (_name)")
1421
+ def generate(code_path: Path, output: Path, stubs: bool, language: tuple, include_private: bool):
1422
+ """Generate documentation from code.
1423
+
1424
+ Creates markdown documentation from code structure.
1425
+
1426
+ Examples:
1427
+ ragtime generate src/ --stubs # Create stub docs
1428
+ ragtime generate src/ -o docs/api # Specify output
1429
+ ragtime generate src/ -l python # Python only
1430
+ """
1431
+ import ast
1432
+ import re as re_module
1433
+
1434
+ code_path = Path(code_path).resolve()
1435
+ output = Path(output)
1436
+
1437
+ if not stubs:
1438
+ click.echo("Use --stubs for stub generation, or /generate-docs for AI-powered docs")
1439
+ click.echo("\nExample: ragtime generate src/ --stubs")
1440
+ return
1441
+
1442
+ # Determine languages
1443
+ if language:
1444
+ languages = list(language)
1445
+ else:
1446
+ languages = ["python", "typescript", "javascript"]
1447
+
1448
+ # Map extensions to languages
1449
+ ext_map = {
1450
+ "python": [".py"],
1451
+ "typescript": [".ts", ".tsx"],
1452
+ "javascript": [".js", ".jsx"],
1453
+ }
1454
+
1455
+ extensions = []
1456
+ for lang in languages:
1457
+ extensions.extend(ext_map.get(lang, []))
1458
+
1459
+ # Find code files
1460
+ code_files = []
1461
+ for ext in extensions:
1462
+ code_files.extend(code_path.rglob(f"*{ext}"))
1463
+
1464
+ # Filter out common exclusions
1465
+ exclude_patterns = ["__pycache__", "node_modules", ".venv", "venv", "dist", "build"]
1466
+ code_files = [
1467
+ f for f in code_files
1468
+ if not any(ex in str(f) for ex in exclude_patterns)
1469
+ ]
1470
+
1471
+ if not code_files:
1472
+ click.echo(f"No code files found in {code_path}")
1473
+ return
1474
+
1475
+ click.echo(f"Found {len(code_files)} code files")
1476
+ click.echo(f"Output: {output}/")
1477
+ click.echo(f"{'─' * 50}")
1478
+
1479
+ output.mkdir(parents=True, exist_ok=True)
1480
+ generated = 0
1481
+
1482
+ for code_file in code_files:
1483
+ try:
1484
+ content = code_file.read_text()
1485
+ except Exception:
1486
+ continue
1487
+
1488
+ relative = code_file.relative_to(code_path)
1489
+ doc_path = output / relative.with_suffix(".md")
1490
+
1491
+ # Parse based on extension
1492
+ if code_file.suffix == ".py":
1493
+ doc_content = generate_python_stub(code_file, content, include_private)
1494
+ elif code_file.suffix in [".ts", ".tsx", ".js", ".jsx"]:
1495
+ doc_content = generate_typescript_stub(code_file, content, include_private)
1496
+ else:
1497
+ continue
1498
+
1499
+ if doc_content:
1500
+ doc_path.parent.mkdir(parents=True, exist_ok=True)
1501
+ doc_path.write_text(doc_content)
1502
+ try:
1503
+ doc_display = doc_path.relative_to(Path.cwd())
1504
+ except ValueError:
1505
+ doc_display = doc_path
1506
+ click.echo(f" ✓ {relative} → {doc_display}")
1507
+ generated += 1
1508
+
1509
+ click.echo(f"\n{'─' * 50}")
1510
+ click.echo(f"✓ Generated {generated} documentation stubs")
1511
+ click.echo(f"\nNext steps:")
1512
+ click.echo(f" 1. Fill in the TODO placeholders")
1513
+ click.echo(f" 2. Or use /generate-docs for AI-generated content")
1514
+ click.echo(f" 3. Run 'ragtime index' to make searchable")
1515
+
1516
+
1517
+ def generate_python_stub(file_path: Path, content: str, include_private: bool) -> str:
1518
+ """Generate markdown stub from Python code."""
1519
+ import ast
1520
+
1521
+ try:
1522
+ tree = ast.parse(content)
1523
+ except SyntaxError:
1524
+ return ""
1525
+
1526
+ lines = []
1527
+ lines.append(f"# {file_path.stem}")
1528
+ lines.append(f"\n> **File:** `{file_path}`")
1529
+ lines.append("\n## Overview\n")
1530
+ lines.append("TODO: Describe what this module does.\n")
1531
+
1532
+ # Get module docstring
1533
+ if ast.get_docstring(tree):
1534
+ lines.append(f"> {ast.get_docstring(tree)}\n")
1535
+
1536
+ classes = []
1537
+ functions = []
1538
+
1539
+ for node in ast.iter_child_nodes(tree):
1540
+ if isinstance(node, ast.ClassDef):
1541
+ if not include_private and node.name.startswith("_"):
1542
+ continue
1543
+ classes.append(node)
1544
+ elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
1545
+ if not include_private and node.name.startswith("_"):
1546
+ continue
1547
+ functions.append(node)
1548
+
1549
+ # Document classes
1550
+ if classes:
1551
+ lines.append("---\n")
1552
+ lines.append("## Classes\n")
1553
+
1554
+ for cls in classes:
1555
+ lines.append(f"### `{cls.name}`\n")
1556
+ if ast.get_docstring(cls):
1557
+ lines.append(f"{ast.get_docstring(cls)}\n")
1558
+ else:
1559
+ lines.append("TODO: Describe this class.\n")
1560
+
1561
+ # Find __init__ and methods
1562
+ methods = []
1563
+ init_node = None
1564
+ for item in cls.body:
1565
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
1566
+ if item.name == "__init__":
1567
+ init_node = item
1568
+ elif not item.name.startswith("_") or include_private:
1569
+ methods.append(item)
1570
+
1571
+ # Constructor
1572
+ if init_node:
1573
+ lines.append("#### Constructor\n")
1574
+ sig = get_function_signature(init_node)
1575
+ lines.append(f"```python\n{sig}\n```\n")
1576
+ params = get_function_params(init_node)
1577
+ if params:
1578
+ lines.append("| Parameter | Type | Default | Description |")
1579
+ lines.append("|-----------|------|---------|-------------|")
1580
+ for p in params:
1581
+ lines.append(f"| `{p['name']}` | `{p['type']}` | {p['default']} | TODO |")
1582
+ lines.append("")
1583
+
1584
+ # Methods
1585
+ if methods:
1586
+ lines.append("#### Methods\n")
1587
+ for method in methods:
1588
+ async_prefix = "async " if isinstance(method, ast.AsyncFunctionDef) else ""
1589
+ ret = get_return_annotation(method)
1590
+ lines.append(f"##### `{async_prefix}{method.name}(...) -> {ret}`\n")
1591
+ if ast.get_docstring(method):
1592
+ lines.append(f"{ast.get_docstring(method)}\n")
1593
+ else:
1594
+ lines.append("TODO: Describe this method.\n")
1595
+
1596
+ # Document functions
1597
+ if functions:
1598
+ lines.append("---\n")
1599
+ lines.append("## Functions\n")
1600
+
1601
+ for func in functions:
1602
+ async_prefix = "async " if isinstance(func, ast.AsyncFunctionDef) else ""
1603
+ ret = get_return_annotation(func)
1604
+ lines.append(f"### `{async_prefix}{func.name}(...) -> {ret}`\n")
1605
+ if ast.get_docstring(func):
1606
+ lines.append(f"{ast.get_docstring(func)}\n")
1607
+ else:
1608
+ lines.append("TODO: Describe this function.\n")
1609
+
1610
+ params = get_function_params(func)
1611
+ if params:
1612
+ lines.append("**Parameters:**\n")
1613
+ for p in params:
1614
+ lines.append(f"- `{p['name']}` (`{p['type']}`): TODO")
1615
+ lines.append("")
1616
+
1617
+ lines.append(f"**Returns:** `{ret}` - TODO\n")
1618
+
1619
+ return "\n".join(lines)
1620
+
1621
+
1622
+ def get_function_signature(node) -> str:
1623
+ """Get function signature string."""
1624
+ import ast
1625
+
1626
+ args = []
1627
+ for arg in node.args.args:
1628
+ if arg.arg == "self":
1629
+ continue
1630
+ type_hint = ""
1631
+ if arg.annotation:
1632
+ type_hint = f": {ast.unparse(arg.annotation)}"
1633
+ args.append(f"{arg.arg}{type_hint}")
1634
+
1635
+ return f"def {node.name}({', '.join(args)})"
1636
+
1637
+
1638
+ def get_function_params(node) -> list:
1639
+ """Get function parameters with types and defaults."""
1640
+ import ast
1641
+
1642
+ params = []
1643
+ defaults = node.args.defaults
1644
+ num_defaults = len(defaults)
1645
+ num_args = len(node.args.args)
1646
+
1647
+ for i, arg in enumerate(node.args.args):
1648
+ if arg.arg in ("self", "cls"):
1649
+ continue
1650
+
1651
+ type_hint = "Any"
1652
+ if arg.annotation:
1653
+ try:
1654
+ type_hint = ast.unparse(arg.annotation)
1655
+ except:
1656
+ type_hint = "Any"
1657
+
1658
+ default = "-"
1659
+ default_idx = i - (num_args - num_defaults)
1660
+ if default_idx >= 0 and default_idx < len(defaults):
1661
+ try:
1662
+ default = f"`{ast.unparse(defaults[default_idx])}`"
1663
+ except:
1664
+ default = "..."
1665
+
1666
+ params.append({
1667
+ "name": arg.arg,
1668
+ "type": type_hint,
1669
+ "default": default,
1670
+ })
1671
+
1672
+ return params
1673
+
1674
+
1675
+ def get_return_annotation(node) -> str:
1676
+ """Get return type annotation."""
1677
+ import ast
1678
+
1679
+ if node.returns:
1680
+ try:
1681
+ return ast.unparse(node.returns)
1682
+ except:
1683
+ return "Any"
1684
+ return "None"
1685
+
1686
+
1687
+ def generate_typescript_stub(file_path: Path, content: str, include_private: bool) -> str:
1688
+ """Generate markdown stub from TypeScript/JavaScript code."""
1689
+ import re as re_module
1690
+
1691
+ lines = []
1692
+ lines.append(f"# {file_path.stem}")
1693
+ lines.append(f"\n> **File:** `{file_path}`")
1694
+ lines.append("\n## Overview\n")
1695
+ lines.append("TODO: Describe what this module does.\n")
1696
+
1697
+ # Find exports using regex
1698
+ class_pattern = r'export\s+(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?'
1699
+ func_pattern = r'export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?'
1700
+ const_pattern = r'export\s+const\s+(\w+)\s*(?::\s*([^=]+))?\s*='
1701
+ interface_pattern = r'export\s+(?:default\s+)?interface\s+(\w+)'
1702
+ type_pattern = r'export\s+type\s+(\w+)'
1703
+
1704
+ classes = re_module.findall(class_pattern, content)
1705
+ functions = re_module.findall(func_pattern, content)
1706
+ consts = re_module.findall(const_pattern, content)
1707
+ interfaces = re_module.findall(interface_pattern, content)
1708
+ types = re_module.findall(type_pattern, content)
1709
+
1710
+ # Interfaces and Types
1711
+ if interfaces or types:
1712
+ lines.append("---\n")
1713
+ lines.append("## Types\n")
1714
+ for iface in interfaces:
1715
+ lines.append(f"### `interface {iface}`\n")
1716
+ lines.append("TODO: Describe this interface.\n")
1717
+ for t in types:
1718
+ lines.append(f"### `type {t}`\n")
1719
+ lines.append("TODO: Describe this type.\n")
1720
+
1721
+ # Classes
1722
+ if classes:
1723
+ lines.append("---\n")
1724
+ lines.append("## Classes\n")
1725
+ for cls_name, extends in classes:
1726
+ lines.append(f"### `{cls_name}`")
1727
+ if extends:
1728
+ lines.append(f" extends `{extends}`")
1729
+ lines.append("\n")
1730
+ lines.append("TODO: Describe this class.\n")
1731
+
1732
+ # Functions
1733
+ if functions:
1734
+ lines.append("---\n")
1735
+ lines.append("## Functions\n")
1736
+ for func_name, params, return_type in functions:
1737
+ ret = return_type.strip() if return_type else "void"
1738
+ lines.append(f"### `{func_name}({params}) => {ret}`\n")
1739
+ lines.append("TODO: Describe this function.\n")
1740
+
1741
+ # Constants
1742
+ if consts:
1743
+ lines.append("---\n")
1744
+ lines.append("## Constants\n")
1745
+ lines.append("| Name | Type | Description |")
1746
+ lines.append("|------|------|-------------|")
1747
+ for const_name, const_type in consts:
1748
+ t = const_type.strip() if const_type else "unknown"
1749
+ lines.append(f"| `{const_name}` | `{t}` | TODO |")
1750
+ lines.append("")
1751
+
1752
+ if len(lines) <= 5: # Only header
1753
+ return ""
1754
+
1755
+ return "\n".join(lines)
1756
+
1757
+
1178
1758
  @main.command()
1179
1759
  @click.argument("docs_path", type=click.Path(exists=True, path_type=Path), default="docs")
1180
1760
  @click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
@@ -1370,7 +1950,7 @@ def update(check: bool):
1370
1950
  from urllib.request import urlopen
1371
1951
  from urllib.error import URLError
1372
1952
 
1373
- current = "0.2.3"
1953
+ current = "0.2.4"
1374
1954
 
1375
1955
  click.echo(f"Current version: {current}")
1376
1956
  click.echo("Checking PyPI for updates...")
@@ -0,0 +1,325 @@
1
+ ---
2
+ description: Generate documentation from code with AI analysis
3
+ allowed-arguments: path to code (e.g., /generate-docs src/)
4
+ allowed-tools: Bash, Read, Write, Edit, AskUserQuestion, Glob, Grep
5
+ ---
6
+
7
+ # Generate Docs
8
+
9
+ Analyze code and generate comprehensive documentation.
10
+
11
+ **Usage:**
12
+ - `/generate-docs src/` - Generate docs for src folder
13
+ - `/generate-docs src/auth/` - Document specific module
14
+ - `/generate-docs` - Interactive mode
15
+
16
+ ## Overview
17
+
18
+ You (Claude) will:
19
+ 1. Read and understand the code
20
+ 2. Write clear, helpful documentation
21
+ 3. Include examples and usage notes
22
+ 4. Output as markdown files
23
+
24
+ This is the AI-powered version. For quick stubs without AI, use `ragtime generate --stubs`.
25
+
26
+ ## Step 1: Get the Code Path
27
+
28
+ **If `$ARGUMENTS` provided:**
29
+ - Use it as the code path
30
+
31
+ **If no arguments:**
32
+ - Ask: "What code should I document? (e.g., src/, src/auth/, specific file)"
33
+
34
+ ## Step 2: Discover Code Files
35
+
36
+ ```bash
37
+ # Find code files
38
+ find {path} -type f \( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" \) \
39
+ -not -path "*/__pycache__/*" \
40
+ -not -path "*/node_modules/*" \
41
+ -not -path "*/.venv/*"
42
+ ```
43
+
44
+ If many files found, ask:
45
+ ```
46
+ Found {count} files. Options:
47
+
48
+ 1. Document all
49
+ 2. Document specific folder
50
+ 3. Document specific files (I'll list them)
51
+ ```
52
+
53
+ ## Step 3: Choose Output Location
54
+
55
+ ```
56
+ Where should I save the documentation?
57
+
58
+ 1. **docs/api/** - API reference docs
59
+ 2. **docs/code/** - Code documentation
60
+ 3. **.ragtime/app/** - As searchable memories
61
+ 4. **Custom path**
62
+ ```
63
+
64
+ ## Step 4: Analyze and Document Each File
65
+
66
+ For each code file:
67
+
68
+ 1. **Read the full file**
69
+ 2. **Understand what it does** - purpose, patterns, relationships
70
+ 3. **Write documentation** that explains:
71
+ - What the module/class/function does
72
+ - Why it exists (context)
73
+ - How to use it (examples)
74
+ - Important details (edge cases, requirements)
75
+
76
+ ### Documentation Quality Guidelines
77
+
78
+ **Good documentation answers:**
79
+ - What does this do?
80
+ - Why would I use it?
81
+ - How do I use it?
82
+ - What should I watch out for?
83
+
84
+ **Avoid:**
85
+ - Just restating the function name ("getUserById gets a user by ID")
86
+ - Obvious descriptions
87
+ - Missing the "why"
88
+
89
+ ### Documentation Template
90
+
91
+ ```markdown
92
+ # {Module Name}
93
+
94
+ > **File:** `{path}`
95
+
96
+ ## Overview
97
+
98
+ {2-3 sentences explaining what this module does and why it exists}
99
+
100
+ ## Quick Start
101
+
102
+ \`\`\`{language}
103
+ {Simple usage example}
104
+ \`\`\`
105
+
106
+ ---
107
+
108
+ ## API Reference
109
+
110
+ ### `ClassName`
111
+
112
+ {Description of the class - what it represents, when to use it}
113
+
114
+ #### Constructor
115
+
116
+ \`\`\`{language}
117
+ {constructor signature}
118
+ \`\`\`
119
+
120
+ | Parameter | Type | Description |
121
+ |-----------|------|-------------|
122
+ | `param` | `Type` | {Actual description} |
123
+
124
+ #### Methods
125
+
126
+ ##### `methodName(params) -> ReturnType`
127
+
128
+ {What this method does, not just restating the name}
129
+
130
+ **Parameters:**
131
+ - `param` (`Type`): {Description}
132
+
133
+ **Returns:** {What it returns and when}
134
+
135
+ **Example:**
136
+ \`\`\`{language}
137
+ {Practical example}
138
+ \`\`\`
139
+
140
+ **Notes:**
141
+ - {Important edge cases}
142
+ - {Performance considerations}
143
+ - {Related methods}
144
+
145
+ ---
146
+
147
+ ## Functions
148
+
149
+ ### `functionName(params) -> ReturnType`
150
+
151
+ {Description}
152
+
153
+ {Parameters, returns, example - same format as methods}
154
+
155
+ ---
156
+
157
+ ## Constants / Configuration
158
+
159
+ | Name | Value | Description |
160
+ |------|-------|-------------|
161
+ | `CONSTANT` | `value` | {What it's for} |
162
+
163
+ ---
164
+
165
+ ## See Also
166
+
167
+ - [{Related Module}]({link})
168
+ - [{External Docs}]({link})
169
+ ```
170
+
171
+ ## Step 5: Add Frontmatter (if .ragtime/ output)
172
+
173
+ ```yaml
174
+ ---
175
+ namespace: app
176
+ type: architecture
177
+ component: {from path}
178
+ source: generate-docs
179
+ confidence: medium
180
+ confidence_reason: ai-generated
181
+ ---
182
+ ```
183
+
184
+ ## Step 6: Write and Report
185
+
186
+ ```
187
+ Generating documentation...
188
+
189
+ ✓ src/auth/jwt.py → docs/api/auth/jwt.md
190
+ - JWTManager class
191
+ - create_token, validate_token functions
192
+
193
+ ✓ src/auth/sessions.py → docs/api/auth/sessions.md
194
+ - SessionStore class
195
+ - Redis integration details
196
+
197
+ ✓ src/db/models.py → docs/api/db/models.md
198
+ - User, Claim, Shift models
199
+ - Relationships documented
200
+ ```
201
+
202
+ ## Step 7: Summary
203
+
204
+ ```
205
+ ───────────────────────────────────────────
206
+ ✅ DOCUMENTATION COMPLETE
207
+ ───────────────────────────────────────────
208
+
209
+ Generated {count} documentation files
210
+
211
+ Coverage:
212
+ - {n} classes documented
213
+ - {n} functions documented
214
+ - {n} with examples
215
+
216
+ Output: {output_path}/
217
+
218
+ Next steps:
219
+ - Review the generated docs
220
+ - Add any missing context
221
+ - Run 'ragtime index' to make searchable
222
+ ```
223
+
224
+ ## Best Practices
225
+
226
+ When writing docs:
227
+
228
+ 1. **Start with the "why"** - Don't just describe what, explain purpose
229
+ 2. **Include real examples** - Show actual usage, not abstract patterns
230
+ 3. **Document edge cases** - What happens with null? Empty arrays?
231
+ 4. **Link related concepts** - Help readers navigate
232
+ 5. **Keep it scannable** - Use headers, tables, code blocks
233
+
234
+ ## Example
235
+
236
+ For this code:
237
+
238
+ ```python
239
+ class RateLimiter:
240
+ """Rate limiting using token bucket algorithm."""
241
+
242
+ def __init__(self, requests_per_minute: int = 60):
243
+ self.rpm = requests_per_minute
244
+ self.tokens = requests_per_minute
245
+ self.last_update = time.time()
246
+
247
+ def allow(self, cost: int = 1) -> bool:
248
+ """Check if request is allowed, consuming tokens if so."""
249
+ self._refill()
250
+ if self.tokens >= cost:
251
+ self.tokens -= cost
252
+ return True
253
+ return False
254
+ ```
255
+
256
+ Generate:
257
+
258
+ ```markdown
259
+ # RateLimiter
260
+
261
+ > **File:** `src/middleware/rate_limit.py`
262
+
263
+ ## Overview
264
+
265
+ Implements rate limiting using the token bucket algorithm. Use this to protect
266
+ APIs from abuse by limiting how many requests a client can make per minute.
267
+
268
+ ## Quick Start
269
+
270
+ \`\`\`python
271
+ limiter = RateLimiter(requests_per_minute=100)
272
+
273
+ @app.before_request
274
+ def check_rate_limit():
275
+ if not limiter.allow():
276
+ return {"error": "Rate limit exceeded"}, 429
277
+ \`\`\`
278
+
279
+ ---
280
+
281
+ ## API Reference
282
+
283
+ ### `RateLimiter`
284
+
285
+ A token bucket rate limiter. Tokens refill continuously over time, allowing
286
+ for burst traffic while maintaining an average rate limit.
287
+
288
+ #### Constructor
289
+
290
+ \`\`\`python
291
+ RateLimiter(requests_per_minute: int = 60)
292
+ \`\`\`
293
+
294
+ | Parameter | Type | Default | Description |
295
+ |-----------|------|---------|-------------|
296
+ | `requests_per_minute` | `int` | `60` | Maximum requests allowed per minute |
297
+
298
+ #### Methods
299
+
300
+ ##### `allow(cost: int = 1) -> bool`
301
+
302
+ Check if a request should be allowed. If allowed, consumes tokens from the bucket.
303
+
304
+ **Parameters:**
305
+ - `cost` (`int`): Number of tokens this request costs. Use higher values for
306
+ expensive operations.
307
+
308
+ **Returns:** `True` if the request is allowed, `False` if rate limited.
309
+
310
+ **Example:**
311
+ \`\`\`python
312
+ # Normal request
313
+ if limiter.allow():
314
+ process_request()
315
+
316
+ # Expensive operation (costs 5 tokens)
317
+ if limiter.allow(cost=5):
318
+ run_expensive_query()
319
+ \`\`\`
320
+
321
+ **Notes:**
322
+ - Tokens refill continuously, not all at once
323
+ - Thread-safe for single-process use
324
+ - For distributed systems, use Redis-backed rate limiting instead
325
+ ```