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.
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/METADATA +179 -42
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/RECORD +8 -7
- src/cli.py +582 -2
- src/commands/generate-docs.md +325 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ragtime-cli
|
|
3
|
-
Version: 0.2.
|
|
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`, `/
|
|
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
|
|
103
|
-
ragtime sync
|
|
152
|
+
# Sync all teammate branch memories
|
|
153
|
+
ragtime sync
|
|
104
154
|
|
|
105
|
-
#
|
|
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
|
-
|
|
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
|
-
.
|
|
152
|
-
├──
|
|
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/
|
|
200
|
+
├── team/ # Team conventions (tracked)
|
|
155
201
|
│ └── {id}-{slug}.md
|
|
156
|
-
├──
|
|
157
|
-
│
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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: '
|
|
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
|
|
216
|
-
| `/
|
|
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.
|
|
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=
|
|
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.
|
|
20
|
-
ragtime_cli-0.2.
|
|
21
|
-
ragtime_cli-0.2.
|
|
22
|
-
ragtime_cli-0.2.
|
|
23
|
-
ragtime_cli-0.2.
|
|
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.
|
|
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.
|
|
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
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|