ragtime-cli 0.2.17__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of ragtime-cli might be problematic. Click here for more details.
- {ragtime_cli-0.2.17.dist-info → ragtime_cli-0.3.0.dist-info}/METADATA +79 -28
- ragtime_cli-0.3.0.dist-info/RECORD +16 -0
- src/cli.py +1021 -111
- src/config.py +17 -1
- src/indexers/docs.py +86 -8
- src/mcp_server.py +26 -3
- ragtime_cli-0.2.17.dist-info/RECORD +0 -26
- src/commands/audit.md +0 -151
- src/commands/create-pr.md +0 -389
- src/commands/generate-docs.md +0 -325
- src/commands/handoff.md +0 -176
- src/commands/import-docs.md +0 -259
- src/commands/pr-graduate.md +0 -192
- src/commands/recall.md +0 -175
- src/commands/remember.md +0 -168
- src/commands/save.md +0 -10
- src/commands/start.md +0 -206
- {ragtime_cli-0.2.17.dist-info → ragtime_cli-0.3.0.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.17.dist-info → ragtime_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.17.dist-info → ragtime_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.17.dist-info → ragtime_cli-0.3.0.dist-info}/top_level.txt +0 -0
src/cli.py
CHANGED
|
@@ -3,6 +3,7 @@ Ragtime CLI - semantic search and memory storage.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
import json
|
|
6
7
|
import subprocess
|
|
7
8
|
import click
|
|
8
9
|
import os
|
|
@@ -168,17 +169,166 @@ def get_remote_branches_with_ragtime(path: Path) -> list[str]:
|
|
|
168
169
|
return []
|
|
169
170
|
|
|
170
171
|
|
|
172
|
+
def setup_mcp_server(project_path: Path, force: bool = False) -> bool:
|
|
173
|
+
"""Offer to configure MCP server for Claude Code integration.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
project_path: The project directory
|
|
177
|
+
force: If True, add MCP server without prompting
|
|
178
|
+
|
|
179
|
+
Returns True if MCP was configured, False otherwise.
|
|
180
|
+
"""
|
|
181
|
+
mcp_config_path = project_path / ".mcp.json"
|
|
182
|
+
|
|
183
|
+
ragtime_config = {
|
|
184
|
+
"command": "ragtime-mcp",
|
|
185
|
+
"args": ["--path", "."]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if mcp_config_path.exists():
|
|
189
|
+
# Read file once
|
|
190
|
+
try:
|
|
191
|
+
existing = json.loads(mcp_config_path.read_text())
|
|
192
|
+
except (IOError, OSError) as e:
|
|
193
|
+
click.echo(f"\n✗ Could not read .mcp.json: {e}", err=True)
|
|
194
|
+
return False
|
|
195
|
+
except json.JSONDecodeError as e:
|
|
196
|
+
click.echo(f"\n! Warning: .mcp.json contains invalid JSON: {e}", err=True)
|
|
197
|
+
if not (force or click.confirm("? Overwrite with new config?", default=False)):
|
|
198
|
+
return False
|
|
199
|
+
existing = {}
|
|
200
|
+
|
|
201
|
+
# Check if ragtime is already configured
|
|
202
|
+
if "mcpServers" in existing and "ragtime" in existing.get("mcpServers", {}):
|
|
203
|
+
click.echo("\n✓ MCP server already configured in .mcp.json")
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
# Add ragtime to existing config
|
|
207
|
+
if force or click.confirm("\n? Add ragtime MCP server to existing .mcp.json?", default=True):
|
|
208
|
+
try:
|
|
209
|
+
if "mcpServers" not in existing:
|
|
210
|
+
existing["mcpServers"] = {}
|
|
211
|
+
existing["mcpServers"]["ragtime"] = ragtime_config
|
|
212
|
+
mcp_config_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
213
|
+
click.echo("\n✓ Added ragtime to .mcp.json")
|
|
214
|
+
return True
|
|
215
|
+
except IOError as e:
|
|
216
|
+
click.echo(f"\n✗ Failed to update .mcp.json: {e}", err=True)
|
|
217
|
+
return False
|
|
218
|
+
else:
|
|
219
|
+
# Create new config
|
|
220
|
+
if force or click.confirm("\n? Create .mcp.json to enable Claude Code MCP integration?", default=True):
|
|
221
|
+
mcp_config = {
|
|
222
|
+
"mcpServers": {
|
|
223
|
+
"ragtime": ragtime_config
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
try:
|
|
227
|
+
mcp_config_path.write_text(json.dumps(mcp_config, indent=2) + "\n")
|
|
228
|
+
click.echo("\n✓ Created .mcp.json with ragtime server")
|
|
229
|
+
return True
|
|
230
|
+
except IOError as e:
|
|
231
|
+
click.echo(f"\n✗ Failed to create .mcp.json: {e}", err=True)
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def setup_mcp_global(force: bool = False) -> bool:
|
|
238
|
+
"""Add ragtime MCP server to global Claude settings.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
force: If True, add without prompting
|
|
242
|
+
|
|
243
|
+
Returns True if configured, False otherwise.
|
|
244
|
+
"""
|
|
245
|
+
claude_dir = Path.home() / ".claude"
|
|
246
|
+
settings_path = claude_dir / "settings.json"
|
|
247
|
+
|
|
248
|
+
ragtime_config = {
|
|
249
|
+
"command": "ragtime-mcp",
|
|
250
|
+
"args": ["--path", "."]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Ensure ~/.claude exists
|
|
254
|
+
try:
|
|
255
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
except OSError as e:
|
|
257
|
+
click.echo(f"✗ Failed to create {claude_dir}: {e}", err=True)
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
if settings_path.exists():
|
|
261
|
+
# Read file once
|
|
262
|
+
try:
|
|
263
|
+
existing = json.loads(settings_path.read_text())
|
|
264
|
+
except (IOError, OSError) as e:
|
|
265
|
+
click.echo(f"✗ Could not read ~/.claude/settings.json: {e}", err=True)
|
|
266
|
+
return False
|
|
267
|
+
except json.JSONDecodeError as e:
|
|
268
|
+
click.echo(f"! Warning: ~/.claude/settings.json contains invalid JSON: {e}", err=True)
|
|
269
|
+
if not (force or click.confirm("? Overwrite with new config?", default=False)):
|
|
270
|
+
return False
|
|
271
|
+
existing = {}
|
|
272
|
+
|
|
273
|
+
# Check if ragtime is already configured
|
|
274
|
+
if "mcpServers" in existing and "ragtime" in existing.get("mcpServers", {}):
|
|
275
|
+
click.echo("✓ MCP server already configured in ~/.claude/settings.json")
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
# Add ragtime to existing config
|
|
279
|
+
if force or click.confirm("? Add ragtime MCP server to ~/.claude/settings.json?", default=True):
|
|
280
|
+
try:
|
|
281
|
+
if "mcpServers" not in existing:
|
|
282
|
+
existing["mcpServers"] = {}
|
|
283
|
+
existing["mcpServers"]["ragtime"] = ragtime_config
|
|
284
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
285
|
+
click.echo("✓ Added ragtime to ~/.claude/settings.json")
|
|
286
|
+
return True
|
|
287
|
+
except IOError as e:
|
|
288
|
+
click.echo(f"✗ Failed to update settings: {e}", err=True)
|
|
289
|
+
return False
|
|
290
|
+
else:
|
|
291
|
+
# Create new settings file
|
|
292
|
+
if force or click.confirm("? Create ~/.claude/settings.json with ragtime MCP server?", default=True):
|
|
293
|
+
settings = {
|
|
294
|
+
"mcpServers": {
|
|
295
|
+
"ragtime": ragtime_config
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
try:
|
|
299
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|
300
|
+
click.echo("✓ Created ~/.claude/settings.json with ragtime server")
|
|
301
|
+
return True
|
|
302
|
+
except IOError as e:
|
|
303
|
+
click.echo(f"✗ Failed to create settings: {e}", err=True)
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
|
|
171
309
|
@click.group()
|
|
172
|
-
@click.version_option(version="0.2.
|
|
173
|
-
|
|
310
|
+
@click.version_option(version="0.2.18")
|
|
311
|
+
@click.option("-y", "--force-defaults", is_flag=True, help="Accept all defaults without prompting")
|
|
312
|
+
@click.pass_context
|
|
313
|
+
def main(ctx, force_defaults: bool):
|
|
174
314
|
"""Ragtime - semantic search over code and documentation."""
|
|
175
|
-
|
|
315
|
+
ctx.ensure_object(dict)
|
|
316
|
+
ctx.obj["force_defaults"] = force_defaults
|
|
176
317
|
|
|
177
318
|
|
|
178
319
|
@main.command()
|
|
179
|
-
@click.argument("path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
180
|
-
|
|
181
|
-
|
|
320
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path), default=".", required=False)
|
|
321
|
+
@click.option("-G", "--global", "global_install", is_flag=True, help="Add MCP server to ~/.claude/settings.json")
|
|
322
|
+
@click.pass_context
|
|
323
|
+
def init(ctx, path: Path, global_install: bool):
|
|
324
|
+
"""Initialize ragtime config for a project, or globally with -G."""
|
|
325
|
+
force = (ctx.obj or {}).get("force_defaults", False)
|
|
326
|
+
|
|
327
|
+
# Global install: just add MCP to ~/.claude/settings.json
|
|
328
|
+
if global_install:
|
|
329
|
+
setup_mcp_global(force=force)
|
|
330
|
+
return
|
|
331
|
+
|
|
182
332
|
path = path.resolve()
|
|
183
333
|
config = init_config(path)
|
|
184
334
|
click.echo(f"Created .ragtime/config.yaml with defaults:")
|
|
@@ -257,6 +407,9 @@ Add your team's conventions above. Each rule should be:
|
|
|
257
407
|
click.echo(f"\n• ghp-cli not found")
|
|
258
408
|
click.echo(f" Install for enhanced workflow: npm install -g @bretwardjames/ghp-cli")
|
|
259
409
|
|
|
410
|
+
# Offer MCP server setup for Claude Code integration
|
|
411
|
+
setup_mcp_server(path, force=(ctx.obj or {}).get("force_defaults", False))
|
|
412
|
+
|
|
260
413
|
|
|
261
414
|
# Batch size for ChromaDB upserts (embedding computation happens here)
|
|
262
415
|
INDEX_BATCH_SIZE = 100
|
|
@@ -664,16 +817,81 @@ def forget(memory_id: str, path: Path):
|
|
|
664
817
|
|
|
665
818
|
|
|
666
819
|
@main.command()
|
|
667
|
-
@click.argument("memory_id")
|
|
820
|
+
@click.argument("memory_id", required=False)
|
|
668
821
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
822
|
+
@click.option("--list", "list_candidates", is_flag=True, help="List graduation candidates")
|
|
823
|
+
@click.option("--branch", "-b", help="Branch name or slug (default: current branch)")
|
|
669
824
|
@click.option("--confidence", default="high",
|
|
670
825
|
type=click.Choice(["high", "medium", "low"]),
|
|
671
826
|
help="Confidence level for graduated memory")
|
|
672
|
-
def graduate(memory_id: str, path: Path, confidence: str):
|
|
673
|
-
"""Graduate a branch memory to app namespace.
|
|
827
|
+
def graduate(memory_id: str, path: Path, list_candidates: bool, branch: str, confidence: str):
|
|
828
|
+
"""Graduate a branch memory to app namespace.
|
|
829
|
+
|
|
830
|
+
With --list: Show branch memories that are candidates for graduation.
|
|
831
|
+
With MEMORY_ID: Graduate a specific memory to app namespace.
|
|
832
|
+
|
|
833
|
+
Examples:
|
|
834
|
+
ragtime graduate --list # List candidates for current branch
|
|
835
|
+
ragtime graduate --list -b feature/auth # List candidates for specific branch
|
|
836
|
+
ragtime graduate abc123 # Graduate specific memory
|
|
837
|
+
"""
|
|
674
838
|
path = Path(path).resolve()
|
|
675
839
|
store = get_memory_store(path)
|
|
676
840
|
|
|
841
|
+
# List mode: show graduation candidates
|
|
842
|
+
if list_candidates or not memory_id:
|
|
843
|
+
# Determine branch
|
|
844
|
+
if not branch:
|
|
845
|
+
branch = get_current_branch(path)
|
|
846
|
+
if not branch:
|
|
847
|
+
click.echo("✗ Not in a git repository or no branch specified", err=True)
|
|
848
|
+
return
|
|
849
|
+
|
|
850
|
+
# Create namespace pattern (handle both original and slugified)
|
|
851
|
+
branch_slug = branch.replace("/", "-")
|
|
852
|
+
namespace = f"branch-{branch_slug}"
|
|
853
|
+
|
|
854
|
+
# Get memories for this branch
|
|
855
|
+
memories = store.list_memories(namespace=namespace, status="active")
|
|
856
|
+
|
|
857
|
+
if not memories:
|
|
858
|
+
click.echo(f"No active memories found for branch: {branch}")
|
|
859
|
+
click.echo(f" (namespace: {namespace})")
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# Filter to graduation candidates (exclude context type)
|
|
863
|
+
candidates = [m for m in memories if m.type != "context"]
|
|
864
|
+
|
|
865
|
+
if not candidates:
|
|
866
|
+
click.echo(f"No graduation candidates for branch: {branch}")
|
|
867
|
+
click.echo(f" (found {len(memories)} memories, but all are context type)")
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
click.echo(f"\nGraduation candidates for branch: {branch}")
|
|
871
|
+
click.echo(f"{'─' * 50}")
|
|
872
|
+
|
|
873
|
+
for i, mem in enumerate(candidates, 1):
|
|
874
|
+
type_badge = f"[{mem.type}]" if mem.type else "[unknown]"
|
|
875
|
+
confidence_badge = f"({mem.confidence})" if hasattr(mem, 'confidence') and mem.confidence else ""
|
|
876
|
+
|
|
877
|
+
click.echo(f"\n {i}. {type_badge} {confidence_badge}")
|
|
878
|
+
click.echo(f" ID: {mem.id}")
|
|
879
|
+
|
|
880
|
+
# Show preview
|
|
881
|
+
preview = mem.content[:150].replace("\n", " ").strip()
|
|
882
|
+
if len(mem.content) > 150:
|
|
883
|
+
preview += "..."
|
|
884
|
+
click.echo(f" {preview}")
|
|
885
|
+
|
|
886
|
+
if hasattr(mem, 'added') and mem.added:
|
|
887
|
+
click.echo(f" Added: {mem.added}")
|
|
888
|
+
|
|
889
|
+
click.echo(f"\n{'─' * 50}")
|
|
890
|
+
click.echo(f"{len(candidates)} candidate(s) found.")
|
|
891
|
+
click.echo(f"\nTo graduate: ragtime graduate <ID>")
|
|
892
|
+
return
|
|
893
|
+
|
|
894
|
+
# Graduate mode: graduate specific memory
|
|
677
895
|
try:
|
|
678
896
|
graduated = store.graduate(memory_id, confidence)
|
|
679
897
|
if graduated:
|
|
@@ -740,6 +958,568 @@ def reindex(path: Path):
|
|
|
740
958
|
click.echo(f"✓ Reindexed {count} memory files")
|
|
741
959
|
|
|
742
960
|
|
|
961
|
+
@main.command("check-conventions")
|
|
962
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
963
|
+
@click.option("--files", "-f", help="Files to check (comma-separated or JSON array)")
|
|
964
|
+
@click.option("--branch", "-b", help="Branch to diff against main (default: current branch)")
|
|
965
|
+
@click.option("--event-file", type=click.Path(exists=True, path_type=Path), help="GHP event file (JSON)")
|
|
966
|
+
@click.option("--include-memories", is_flag=True, default=True, help="Include convention memories")
|
|
967
|
+
@click.option("--all", "return_all", is_flag=True, help="Return ALL conventions (for AI workflows)")
|
|
968
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON (for MCP/hooks)")
|
|
969
|
+
def check_conventions(path: Path, files: str, branch: str, event_file: Path,
|
|
970
|
+
include_memories: bool, return_all: bool, json_output: bool):
|
|
971
|
+
"""Show conventions applicable to changed files.
|
|
972
|
+
|
|
973
|
+
Used by pre-PR hooks to check code against team conventions.
|
|
974
|
+
|
|
975
|
+
By default, returns only conventions RELEVANT to the changed files (semantic search).
|
|
976
|
+
Use --all to return ALL conventions (useful for AI workflows that analyze edge cases).
|
|
977
|
+
|
|
978
|
+
Examples:
|
|
979
|
+
ragtime check-conventions # Relevant conventions only
|
|
980
|
+
ragtime check-conventions --all # ALL conventions (for AI)
|
|
981
|
+
ragtime check-conventions -f "src/auth.ts" # Conventions relevant to specific file
|
|
982
|
+
ragtime check-conventions --event-file /tmp/ghp-event.json --all # From GHP hook
|
|
983
|
+
"""
|
|
984
|
+
import json as json_module
|
|
985
|
+
|
|
986
|
+
path = Path(path).resolve()
|
|
987
|
+
config = RagtimeConfig.load(path)
|
|
988
|
+
|
|
989
|
+
# Load from event file if provided (GHP hook pattern)
|
|
990
|
+
if event_file:
|
|
991
|
+
try:
|
|
992
|
+
event_data = json_module.loads(Path(event_file).read_text())
|
|
993
|
+
# Event file can provide changed_files and branch
|
|
994
|
+
if not files and "changed_files" in event_data:
|
|
995
|
+
files = event_data["changed_files"]
|
|
996
|
+
if not branch and "branch" in event_data:
|
|
997
|
+
branch = event_data["branch"]
|
|
998
|
+
except (json_module.JSONDecodeError, IOError) as e:
|
|
999
|
+
click.echo(f"⚠ Could not read event file: {e}", err=True)
|
|
1000
|
+
|
|
1001
|
+
# Determine files to check
|
|
1002
|
+
changed_files = []
|
|
1003
|
+
if files:
|
|
1004
|
+
# Handle list (from event file) or string (from CLI)
|
|
1005
|
+
if isinstance(files, list):
|
|
1006
|
+
changed_files = files
|
|
1007
|
+
elif files.startswith("["):
|
|
1008
|
+
try:
|
|
1009
|
+
changed_files = json_module.loads(files)
|
|
1010
|
+
except json_module.JSONDecodeError:
|
|
1011
|
+
changed_files = [f.strip() for f in files.split(",")]
|
|
1012
|
+
else:
|
|
1013
|
+
changed_files = [f.strip() for f in files.split(",")]
|
|
1014
|
+
else:
|
|
1015
|
+
# Get changed files from git diff
|
|
1016
|
+
if not branch:
|
|
1017
|
+
branch = get_current_branch(path)
|
|
1018
|
+
if branch and branch not in ("main", "master"):
|
|
1019
|
+
result = subprocess.run(
|
|
1020
|
+
["git", "diff", "--name-only", "main...HEAD"],
|
|
1021
|
+
cwd=path,
|
|
1022
|
+
capture_output=True,
|
|
1023
|
+
text=True,
|
|
1024
|
+
)
|
|
1025
|
+
if result.returncode == 0:
|
|
1026
|
+
changed_files = [f for f in result.stdout.strip().split("\n") if f]
|
|
1027
|
+
|
|
1028
|
+
# Gather conventions from files
|
|
1029
|
+
conventions_data = {
|
|
1030
|
+
"files": [],
|
|
1031
|
+
"memories": [],
|
|
1032
|
+
"changed_files": changed_files,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
# Read convention files from config
|
|
1036
|
+
for conv_file in config.conventions.files:
|
|
1037
|
+
conv_path = path / conv_file
|
|
1038
|
+
if conv_path.exists():
|
|
1039
|
+
if conv_path.is_file():
|
|
1040
|
+
content = conv_path.read_text()
|
|
1041
|
+
conventions_data["files"].append({
|
|
1042
|
+
"path": str(conv_file),
|
|
1043
|
+
"content": content,
|
|
1044
|
+
})
|
|
1045
|
+
elif conv_path.is_dir():
|
|
1046
|
+
# If it's a directory, read all files in it
|
|
1047
|
+
for f in conv_path.rglob("*"):
|
|
1048
|
+
if f.is_file() and f.suffix in (".md", ".txt", ".yaml", ".yml"):
|
|
1049
|
+
content = f.read_text()
|
|
1050
|
+
conventions_data["files"].append({
|
|
1051
|
+
"path": str(f.relative_to(path)),
|
|
1052
|
+
"content": content,
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
# Search memories for conventions
|
|
1056
|
+
if include_memories and config.conventions.also_search_memories:
|
|
1057
|
+
store = get_memory_store(path)
|
|
1058
|
+
db = get_db(path)
|
|
1059
|
+
|
|
1060
|
+
if return_all:
|
|
1061
|
+
# Return ALL convention/pattern memories (for AI workflows)
|
|
1062
|
+
for ns in ["team", "app"]:
|
|
1063
|
+
memories = store.list_memories(namespace=ns, type_filter="convention", status="active")
|
|
1064
|
+
for mem in memories:
|
|
1065
|
+
conventions_data["memories"].append({
|
|
1066
|
+
"id": mem.id,
|
|
1067
|
+
"namespace": mem.namespace,
|
|
1068
|
+
"content": mem.content,
|
|
1069
|
+
"component": mem.component,
|
|
1070
|
+
})
|
|
1071
|
+
# Also get pattern-type memories
|
|
1072
|
+
patterns = store.list_memories(namespace=ns, type_filter="pattern", status="active")
|
|
1073
|
+
for mem in patterns:
|
|
1074
|
+
conventions_data["memories"].append({
|
|
1075
|
+
"id": mem.id,
|
|
1076
|
+
"namespace": mem.namespace,
|
|
1077
|
+
"type": "pattern",
|
|
1078
|
+
"content": mem.content,
|
|
1079
|
+
"component": mem.component,
|
|
1080
|
+
})
|
|
1081
|
+
else:
|
|
1082
|
+
# Semantic search for RELEVANT conventions based on changed files
|
|
1083
|
+
if changed_files:
|
|
1084
|
+
# Build search query from file paths and names
|
|
1085
|
+
# Extract meaningful terms: paths, extensions, inferred components
|
|
1086
|
+
search_terms = set()
|
|
1087
|
+
for f in changed_files:
|
|
1088
|
+
parts = Path(f).parts
|
|
1089
|
+
search_terms.update(parts) # Add path components
|
|
1090
|
+
search_terms.add(Path(f).suffix.lstrip(".")) # Add extension
|
|
1091
|
+
search_terms.add(Path(f).stem) # Add filename without ext
|
|
1092
|
+
|
|
1093
|
+
# Remove common noise
|
|
1094
|
+
noise = {"src", "lib", "test", "tests", "spec", "specs", "", "js", "ts", "py", "md"}
|
|
1095
|
+
search_terms = search_terms - noise
|
|
1096
|
+
|
|
1097
|
+
query = f"conventions patterns rules standards for {' '.join(search_terms)}"
|
|
1098
|
+
|
|
1099
|
+
# Search both namespaces using the db directly
|
|
1100
|
+
seen_ids = set()
|
|
1101
|
+
for ns in ["team", "app"]:
|
|
1102
|
+
results = db.search(query, namespace=ns, limit=50)
|
|
1103
|
+
for result in results:
|
|
1104
|
+
meta = result.get("metadata", {})
|
|
1105
|
+
result_type = meta.get("type", "")
|
|
1106
|
+
result_category = meta.get("category", "")
|
|
1107
|
+
# Include convention/pattern types OR docs with category="convention"
|
|
1108
|
+
is_convention_content = (
|
|
1109
|
+
result_type in ("convention", "pattern") or
|
|
1110
|
+
result_category == "convention"
|
|
1111
|
+
)
|
|
1112
|
+
if is_convention_content:
|
|
1113
|
+
mem_id = meta.get("id") or meta.get("file", "")
|
|
1114
|
+
if mem_id not in seen_ids:
|
|
1115
|
+
seen_ids.add(mem_id)
|
|
1116
|
+
distance = result.get("distance", 1)
|
|
1117
|
+
score = 1 - distance if distance else 0
|
|
1118
|
+
conventions_data["memories"].append({
|
|
1119
|
+
"id": mem_id,
|
|
1120
|
+
"namespace": ns,
|
|
1121
|
+
"type": result_type,
|
|
1122
|
+
"content": result.get("content", ""),
|
|
1123
|
+
"component": meta.get("component"),
|
|
1124
|
+
"relevance": score,
|
|
1125
|
+
})
|
|
1126
|
+
else:
|
|
1127
|
+
# No changed files - fall back to listing all
|
|
1128
|
+
for ns in ["team", "app"]:
|
|
1129
|
+
memories = store.list_memories(namespace=ns, type_filter="convention", status="active")
|
|
1130
|
+
for mem in memories:
|
|
1131
|
+
conventions_data["memories"].append({
|
|
1132
|
+
"id": mem.id,
|
|
1133
|
+
"namespace": mem.namespace,
|
|
1134
|
+
"content": mem.content,
|
|
1135
|
+
"component": mem.component,
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
# Output
|
|
1139
|
+
if json_output:
|
|
1140
|
+
click.echo(json_module.dumps(conventions_data, indent=2))
|
|
1141
|
+
return
|
|
1142
|
+
|
|
1143
|
+
# Human-friendly output
|
|
1144
|
+
click.echo(f"\nConventions Check")
|
|
1145
|
+
click.echo(f"{'═' * 50}")
|
|
1146
|
+
|
|
1147
|
+
if changed_files:
|
|
1148
|
+
click.echo(f"\nFiles to check ({len(changed_files)}):")
|
|
1149
|
+
for f in changed_files[:10]:
|
|
1150
|
+
click.echo(f" • {f}")
|
|
1151
|
+
if len(changed_files) > 10:
|
|
1152
|
+
click.echo(f" ... and {len(changed_files) - 10} more")
|
|
1153
|
+
|
|
1154
|
+
click.echo(f"\n{'─' * 50}")
|
|
1155
|
+
click.echo(f"Convention Files ({len(conventions_data['files'])}):")
|
|
1156
|
+
|
|
1157
|
+
if not conventions_data["files"]:
|
|
1158
|
+
click.echo(" No convention files found.")
|
|
1159
|
+
click.echo(f" Configure in .ragtime/config.yaml under 'conventions.files'")
|
|
1160
|
+
else:
|
|
1161
|
+
for conv in conventions_data["files"]:
|
|
1162
|
+
click.echo(f"\n 📄 {conv['path']}")
|
|
1163
|
+
# Show first few lines as preview
|
|
1164
|
+
lines = conv["content"].strip().split("\n")[:5]
|
|
1165
|
+
for line in lines:
|
|
1166
|
+
if line.strip():
|
|
1167
|
+
click.echo(f" {line[:70]}{'...' if len(line) > 70 else ''}")
|
|
1168
|
+
|
|
1169
|
+
if conventions_data["memories"]:
|
|
1170
|
+
click.echo(f"\n{'─' * 50}")
|
|
1171
|
+
mode_label = "All" if return_all else "Relevant"
|
|
1172
|
+
click.echo(f"Convention Memories - {mode_label} ({len(conventions_data['memories'])}):")
|
|
1173
|
+
|
|
1174
|
+
for mem in conventions_data["memories"]:
|
|
1175
|
+
type_str = mem.get("type", "convention")
|
|
1176
|
+
relevance = mem.get("relevance")
|
|
1177
|
+
relevance_str = f" (relevance: {relevance:.2f})" if relevance else ""
|
|
1178
|
+
click.echo(f"\n [{mem['id'][:8] if mem['id'] else '?'}] {mem['namespace']} / {type_str}{relevance_str}")
|
|
1179
|
+
if mem.get("component"):
|
|
1180
|
+
click.echo(f" Component: {mem['component']}")
|
|
1181
|
+
preview = mem["content"][:100].replace("\n", " ")
|
|
1182
|
+
click.echo(f" {preview}...")
|
|
1183
|
+
|
|
1184
|
+
click.echo(f"\n{'═' * 50}")
|
|
1185
|
+
total = len(conventions_data["files"]) + len(conventions_data["memories"])
|
|
1186
|
+
mode_note = " (all)" if return_all else " (filtered by relevance)"
|
|
1187
|
+
click.echo(f"Total: {total} convention source(s){mode_note}")
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
@main.command("add-convention")
|
|
1191
|
+
@click.argument("content", required=False)
|
|
1192
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1193
|
+
@click.option("--component", "-c", help="Component this convention applies to")
|
|
1194
|
+
@click.option("--to-file", type=click.Path(path_type=Path), help="Add to specific file")
|
|
1195
|
+
@click.option("--to-memory", is_flag=True, help="Store as memory only (not in git)")
|
|
1196
|
+
@click.option("--quiet", "-q", is_flag=True, help="Non-interactive mode")
|
|
1197
|
+
def add_convention(content: str | None, path: Path, component: str | None,
|
|
1198
|
+
to_file: Path | None, to_memory: bool, quiet: bool):
|
|
1199
|
+
"""Add a new convention with smart storage routing.
|
|
1200
|
+
|
|
1201
|
+
Finds the best place to store the convention:
|
|
1202
|
+
- Existing doc with ## Conventions section (if component matches)
|
|
1203
|
+
- Convention folder (.ragtime/conventions/)
|
|
1204
|
+
- Central conventions file (.ragtime/CONVENTIONS.md)
|
|
1205
|
+
- Memory only (searchable but not committed)
|
|
1206
|
+
|
|
1207
|
+
Examples:
|
|
1208
|
+
ragtime add-convention "Always use async/await, never .then()"
|
|
1209
|
+
ragtime add-convention --component auth "JWT tokens expire after 15 minutes"
|
|
1210
|
+
ragtime add-convention --to-file docs/api.md "Use snake_case for JSON"
|
|
1211
|
+
ragtime add-convention --to-memory "Prefer composition over inheritance"
|
|
1212
|
+
"""
|
|
1213
|
+
path = Path(path).resolve()
|
|
1214
|
+
config = RagtimeConfig.load(path)
|
|
1215
|
+
|
|
1216
|
+
# Get content interactively if not provided
|
|
1217
|
+
if not content:
|
|
1218
|
+
content = click.prompt("Convention to add")
|
|
1219
|
+
|
|
1220
|
+
if not content.strip():
|
|
1221
|
+
click.echo("✗ No content provided", err=True)
|
|
1222
|
+
return
|
|
1223
|
+
|
|
1224
|
+
# If explicit destination provided, use it
|
|
1225
|
+
if to_memory:
|
|
1226
|
+
_store_convention_as_memory(path, content, component)
|
|
1227
|
+
return
|
|
1228
|
+
|
|
1229
|
+
if to_file:
|
|
1230
|
+
_append_convention_to_file(path, to_file, content)
|
|
1231
|
+
return
|
|
1232
|
+
|
|
1233
|
+
# Auto-routing based on config
|
|
1234
|
+
storage_mode = config.conventions.storage
|
|
1235
|
+
|
|
1236
|
+
if storage_mode == "memory":
|
|
1237
|
+
_store_convention_as_memory(path, content, component)
|
|
1238
|
+
return
|
|
1239
|
+
|
|
1240
|
+
# Find storage options
|
|
1241
|
+
options = _find_convention_storage_options(path, config, component)
|
|
1242
|
+
|
|
1243
|
+
if storage_mode == "file" or (storage_mode == "auto" and options):
|
|
1244
|
+
# Use first available file option, or default
|
|
1245
|
+
if options:
|
|
1246
|
+
target = options[0]
|
|
1247
|
+
else:
|
|
1248
|
+
target = {
|
|
1249
|
+
"type": "default_file",
|
|
1250
|
+
"path": config.conventions.default_file,
|
|
1251
|
+
"description": "Central conventions file",
|
|
1252
|
+
}
|
|
1253
|
+
_store_convention_to_target(path, target, content, component)
|
|
1254
|
+
return
|
|
1255
|
+
|
|
1256
|
+
if storage_mode == "ask" or (storage_mode == "auto" and not quiet):
|
|
1257
|
+
# Present options to user
|
|
1258
|
+
click.echo("\nWhere should this convention be stored?\n")
|
|
1259
|
+
|
|
1260
|
+
choices = []
|
|
1261
|
+
for i, opt in enumerate(options, 1):
|
|
1262
|
+
icon = "📄" if opt["type"] == "existing_section" else "📁" if opt["type"] == "folder" else "📋"
|
|
1263
|
+
click.echo(f" {i}. {icon} {opt['description']}")
|
|
1264
|
+
click.echo(f" {opt['path']}")
|
|
1265
|
+
choices.append(opt)
|
|
1266
|
+
|
|
1267
|
+
# Add default options if not already present
|
|
1268
|
+
default_file_present = any(o["path"] == config.conventions.default_file for o in choices)
|
|
1269
|
+
if not default_file_present:
|
|
1270
|
+
i = len(choices) + 1
|
|
1271
|
+
click.echo(f" {i}. 📋 Central conventions file")
|
|
1272
|
+
click.echo(f" {config.conventions.default_file}")
|
|
1273
|
+
choices.append({
|
|
1274
|
+
"type": "default_file",
|
|
1275
|
+
"path": config.conventions.default_file,
|
|
1276
|
+
"description": "Central conventions file",
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
i = len(choices) + 1
|
|
1280
|
+
click.echo(f" {i}. 🧠 Memory only (not committed)")
|
|
1281
|
+
click.echo(f" Stored in team namespace, searchable but not in git")
|
|
1282
|
+
choices.append({"type": "memory", "path": None, "description": "Memory only"})
|
|
1283
|
+
|
|
1284
|
+
click.echo()
|
|
1285
|
+
choice = click.prompt("Choice", type=int, default=1)
|
|
1286
|
+
|
|
1287
|
+
if choice < 1 or choice > len(choices):
|
|
1288
|
+
click.echo("✗ Invalid choice", err=True)
|
|
1289
|
+
return
|
|
1290
|
+
|
|
1291
|
+
target = choices[choice - 1]
|
|
1292
|
+
if target["type"] == "memory":
|
|
1293
|
+
_store_convention_as_memory(path, content, component)
|
|
1294
|
+
else:
|
|
1295
|
+
_store_convention_to_target(path, target, content, component)
|
|
1296
|
+
return
|
|
1297
|
+
|
|
1298
|
+
# Fallback: memory only
|
|
1299
|
+
_store_convention_as_memory(path, content, component)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _find_convention_storage_options(path: Path, config: RagtimeConfig, component: str | None) -> list[dict]:
|
|
1303
|
+
"""Find potential storage locations for a convention."""
|
|
1304
|
+
import re
|
|
1305
|
+
options = []
|
|
1306
|
+
|
|
1307
|
+
# 1. Check for existing docs with ## Conventions sections
|
|
1308
|
+
for scan_path in config.conventions.scan_docs_for_sections:
|
|
1309
|
+
scan_dir = path / scan_path
|
|
1310
|
+
if scan_dir.exists() and scan_dir.is_dir():
|
|
1311
|
+
for md_file in scan_dir.rglob("*.md"):
|
|
1312
|
+
content = md_file.read_text()
|
|
1313
|
+
# Look for ## Conventions header
|
|
1314
|
+
if re.search(r'^##\s+(Conventions?|Rules|Standards|Guidelines)', content, re.MULTILINE | re.IGNORECASE):
|
|
1315
|
+
# If component specified, check if file relates to it
|
|
1316
|
+
file_component = md_file.stem.lower()
|
|
1317
|
+
rel_path = md_file.relative_to(path)
|
|
1318
|
+
|
|
1319
|
+
if component:
|
|
1320
|
+
if component.lower() in str(rel_path).lower():
|
|
1321
|
+
options.insert(0, { # Prioritize component match
|
|
1322
|
+
"type": "existing_section",
|
|
1323
|
+
"path": str(rel_path),
|
|
1324
|
+
"description": f"Add to existing Conventions section ({file_component})",
|
|
1325
|
+
})
|
|
1326
|
+
else:
|
|
1327
|
+
options.append({
|
|
1328
|
+
"type": "existing_section",
|
|
1329
|
+
"path": str(rel_path),
|
|
1330
|
+
"description": f"Add to existing Conventions section",
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
# 2. Check if convention folder exists
|
|
1334
|
+
folder = path / config.conventions.folder
|
|
1335
|
+
if folder.exists() and folder.is_dir():
|
|
1336
|
+
if component:
|
|
1337
|
+
target_file = f"{component.lower()}.md"
|
|
1338
|
+
options.append({
|
|
1339
|
+
"type": "folder",
|
|
1340
|
+
"path": f"{config.conventions.folder}{target_file}",
|
|
1341
|
+
"description": f"Create/append to {target_file} in conventions folder",
|
|
1342
|
+
})
|
|
1343
|
+
else:
|
|
1344
|
+
options.append({
|
|
1345
|
+
"type": "folder",
|
|
1346
|
+
"path": f"{config.conventions.folder}general.md",
|
|
1347
|
+
"description": "Add to general.md in conventions folder",
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
# 3. Check if default conventions file exists
|
|
1351
|
+
default_file = path / config.conventions.default_file
|
|
1352
|
+
if default_file.exists():
|
|
1353
|
+
options.append({
|
|
1354
|
+
"type": "default_file",
|
|
1355
|
+
"path": config.conventions.default_file,
|
|
1356
|
+
"description": "Append to central conventions file",
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
return options
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _store_convention_as_memory(path: Path, content: str, component: str | None):
|
|
1363
|
+
"""Store convention as a memory in team namespace."""
|
|
1364
|
+
from datetime import date
|
|
1365
|
+
store = get_memory_store(path)
|
|
1366
|
+
|
|
1367
|
+
memory = Memory(
|
|
1368
|
+
content=content,
|
|
1369
|
+
namespace="team",
|
|
1370
|
+
type="convention",
|
|
1371
|
+
component=component,
|
|
1372
|
+
confidence="high",
|
|
1373
|
+
confidence_reason="user-added",
|
|
1374
|
+
source="add-convention",
|
|
1375
|
+
status="active",
|
|
1376
|
+
added=date.today().isoformat(),
|
|
1377
|
+
author=get_author(),
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1380
|
+
file_path = store.save(memory)
|
|
1381
|
+
click.echo(f"✓ Convention stored as memory")
|
|
1382
|
+
click.echo(f" ID: {memory.id}")
|
|
1383
|
+
click.echo(f" File: {file_path.relative_to(path)}")
|
|
1384
|
+
click.echo(f" Namespace: team")
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def _store_convention_to_target(path: Path, target: dict, content: str, component: str | None):
|
|
1388
|
+
"""Store convention to a file target."""
|
|
1389
|
+
import re
|
|
1390
|
+
|
|
1391
|
+
target_path = path / target["path"]
|
|
1392
|
+
target_type = target["type"]
|
|
1393
|
+
|
|
1394
|
+
if target_type == "existing_section":
|
|
1395
|
+
# Append to existing ## Conventions section
|
|
1396
|
+
if not target_path.exists():
|
|
1397
|
+
click.echo(f"✗ File not found: {target['path']}", err=True)
|
|
1398
|
+
return
|
|
1399
|
+
|
|
1400
|
+
file_content = target_path.read_text()
|
|
1401
|
+
|
|
1402
|
+
# Find the Conventions section and append
|
|
1403
|
+
pattern = r'(^##\s+(?:Conventions?|Rules|Standards|Guidelines).*?)(\n##|\Z)'
|
|
1404
|
+
match = re.search(pattern, file_content, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1405
|
+
|
|
1406
|
+
if match:
|
|
1407
|
+
section_end = match.end(1)
|
|
1408
|
+
new_content = file_content[:section_end] + f"\n\n- {content}" + file_content[section_end:]
|
|
1409
|
+
target_path.write_text(new_content)
|
|
1410
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1411
|
+
click.echo(f" Added to ## Conventions section")
|
|
1412
|
+
else:
|
|
1413
|
+
click.echo(f"✗ Could not find Conventions section in {target['path']}", err=True)
|
|
1414
|
+
|
|
1415
|
+
elif target_type == "folder":
|
|
1416
|
+
# Create or append to file in conventions folder
|
|
1417
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1418
|
+
|
|
1419
|
+
if target_path.exists():
|
|
1420
|
+
# Append to existing file
|
|
1421
|
+
existing = target_path.read_text()
|
|
1422
|
+
new_content = existing.rstrip() + f"\n\n- {content}\n"
|
|
1423
|
+
else:
|
|
1424
|
+
# Create new file
|
|
1425
|
+
title = target_path.stem.replace("-", " ").replace("_", " ").title()
|
|
1426
|
+
new_content = f"""---
|
|
1427
|
+
namespace: team
|
|
1428
|
+
type: convention
|
|
1429
|
+
component: {component or ''}
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
# {title} Conventions
|
|
1433
|
+
|
|
1434
|
+
- {content}
|
|
1435
|
+
"""
|
|
1436
|
+
target_path.write_text(new_content)
|
|
1437
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1438
|
+
|
|
1439
|
+
elif target_type == "default_file":
|
|
1440
|
+
# Create or append to default conventions file
|
|
1441
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1442
|
+
|
|
1443
|
+
if target_path.exists():
|
|
1444
|
+
existing = target_path.read_text()
|
|
1445
|
+
# Check if there's a component section
|
|
1446
|
+
if component:
|
|
1447
|
+
section_pattern = rf'^##\s+{re.escape(component)}.*?(?=\n##|\Z)'
|
|
1448
|
+
match = re.search(section_pattern, existing, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1449
|
+
if match:
|
|
1450
|
+
# Append to component section
|
|
1451
|
+
section_end = match.end()
|
|
1452
|
+
new_content = existing[:section_end] + f"\n- {content}" + existing[section_end:]
|
|
1453
|
+
else:
|
|
1454
|
+
# Create new component section
|
|
1455
|
+
new_content = existing.rstrip() + f"\n\n## {component.title()}\n\n- {content}\n"
|
|
1456
|
+
else:
|
|
1457
|
+
# Append to general section or end
|
|
1458
|
+
new_content = existing.rstrip() + f"\n\n- {content}\n"
|
|
1459
|
+
else:
|
|
1460
|
+
# Create new file
|
|
1461
|
+
header = f"## {component.title()}\n\n" if component else ""
|
|
1462
|
+
new_content = f"""# Team Conventions
|
|
1463
|
+
|
|
1464
|
+
{header}- {content}
|
|
1465
|
+
"""
|
|
1466
|
+
target_path.write_text(new_content)
|
|
1467
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1468
|
+
|
|
1469
|
+
# Reindex the file
|
|
1470
|
+
_reindex_convention_file(path, target_path)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _append_convention_to_file(path: Path, to_file: Path, content: str):
|
|
1474
|
+
"""Append a convention directly to a user-specified file."""
|
|
1475
|
+
import re
|
|
1476
|
+
|
|
1477
|
+
target_path = path / to_file if not to_file.is_absolute() else to_file
|
|
1478
|
+
|
|
1479
|
+
if not target_path.exists():
|
|
1480
|
+
click.echo(f"✗ File not found: {to_file}", err=True)
|
|
1481
|
+
return
|
|
1482
|
+
|
|
1483
|
+
file_content = target_path.read_text()
|
|
1484
|
+
|
|
1485
|
+
# Try to find a Conventions section to append to
|
|
1486
|
+
pattern = r'(^##\s+(?:Conventions?|Rules|Standards|Guidelines).*?)(\n##|\Z)'
|
|
1487
|
+
match = re.search(pattern, file_content, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1488
|
+
|
|
1489
|
+
if match:
|
|
1490
|
+
# Append to existing section
|
|
1491
|
+
section_end = match.end(1)
|
|
1492
|
+
new_content = file_content[:section_end] + f"\n\n- {content}" + file_content[section_end:]
|
|
1493
|
+
target_path.write_text(new_content)
|
|
1494
|
+
click.echo(f"✓ Convention added to {to_file}")
|
|
1495
|
+
click.echo(f" Added to ## Conventions section")
|
|
1496
|
+
else:
|
|
1497
|
+
# Append at end of file
|
|
1498
|
+
new_content = file_content.rstrip() + f"\n\n- {content}\n"
|
|
1499
|
+
target_path.write_text(new_content)
|
|
1500
|
+
click.echo(f"✓ Convention appended to {to_file}")
|
|
1501
|
+
|
|
1502
|
+
# Reindex the file
|
|
1503
|
+
_reindex_convention_file(path, target_path)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _reindex_convention_file(path: Path, target_path: Path):
|
|
1507
|
+
"""Reindex a convention file after modification."""
|
|
1508
|
+
try:
|
|
1509
|
+
db = get_db(path)
|
|
1510
|
+
from .indexers.docs import index_file
|
|
1511
|
+
entries = index_file(target_path)
|
|
1512
|
+
for entry in entries:
|
|
1513
|
+
db.upsert(
|
|
1514
|
+
ids=[f"{entry.file_path}:{entry.chunk_index}"],
|
|
1515
|
+
documents=[entry.content],
|
|
1516
|
+
metadatas=[entry.to_metadata()],
|
|
1517
|
+
)
|
|
1518
|
+
click.echo(f" Indexed {len(entries)} section(s)")
|
|
1519
|
+
except Exception as e:
|
|
1520
|
+
click.echo(f" ⚠ Could not reindex: {e}")
|
|
1521
|
+
|
|
1522
|
+
|
|
743
1523
|
@main.command()
|
|
744
1524
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
745
1525
|
@click.option("--dry-run", is_flag=True, help="Show what would be removed")
|
|
@@ -867,9 +1647,9 @@ def new_branch(issue: int, path: Path, content: str, issue_json: str, branch: st
|
|
|
867
1647
|
click.echo(f"✗ Could not fetch issue #{issue}", err=True)
|
|
868
1648
|
return
|
|
869
1649
|
|
|
870
|
-
title = issue_data.get("title"
|
|
871
|
-
body = issue_data.get("body"
|
|
872
|
-
labels = issue_data.get("labels"
|
|
1650
|
+
title = issue_data.get("title") or f"Issue #{issue}"
|
|
1651
|
+
body = issue_data.get("body") or "" # Handle null and empty
|
|
1652
|
+
labels = issue_data.get("labels") or []
|
|
873
1653
|
|
|
874
1654
|
if labels:
|
|
875
1655
|
if isinstance(labels[0], dict):
|
|
@@ -923,115 +1703,244 @@ author: {get_author()}
|
|
|
923
1703
|
|
|
924
1704
|
|
|
925
1705
|
# ============================================================================
|
|
926
|
-
#
|
|
1706
|
+
# Usage Documentation
|
|
927
1707
|
# ============================================================================
|
|
928
1708
|
|
|
929
1709
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1710
|
+
@main.command("usage")
|
|
1711
|
+
@click.option("--section", "-s", help="Show specific section (mcp, cli, workflows, conventions)")
|
|
1712
|
+
def usage(section: str | None):
|
|
1713
|
+
"""Show how to use ragtime effectively with AI agents.
|
|
933
1714
|
|
|
1715
|
+
Prints documentation on integrating ragtime into your AI workflow,
|
|
1716
|
+
whether using the MCP server or CLI directly.
|
|
1717
|
+
"""
|
|
1718
|
+
sections = {
|
|
1719
|
+
"mcp": USAGE_MCP,
|
|
1720
|
+
"cli": USAGE_CLI,
|
|
1721
|
+
"workflows": USAGE_WORKFLOWS,
|
|
1722
|
+
"conventions": USAGE_CONVENTIONS,
|
|
1723
|
+
}
|
|
934
1724
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
@main.command("install")
|
|
944
|
-
@click.option("--global", "global_install", is_flag=True, help="Install to ~/.claude/commands/")
|
|
945
|
-
@click.option("--workspace", "workspace_install", is_flag=True, help="Install to .claude/commands/")
|
|
946
|
-
@click.option("--list", "list_commands", is_flag=True, help="List available commands")
|
|
947
|
-
@click.option("--force", is_flag=True, help="Overwrite existing commands without asking")
|
|
948
|
-
@click.argument("commands", nargs=-1)
|
|
949
|
-
def install_commands(global_install: bool, workspace_install: bool, list_commands: bool,
|
|
950
|
-
force: bool, commands: tuple):
|
|
951
|
-
"""Install Claude command templates."""
|
|
952
|
-
available = get_available_commands()
|
|
953
|
-
|
|
954
|
-
if list_commands:
|
|
955
|
-
click.echo("Available commands:")
|
|
956
|
-
for cmd in available:
|
|
957
|
-
click.echo(f" - {cmd}")
|
|
1725
|
+
if section:
|
|
1726
|
+
if section.lower() in sections:
|
|
1727
|
+
click.echo(sections[section.lower()])
|
|
1728
|
+
else:
|
|
1729
|
+
click.echo(f"Unknown section: {section}")
|
|
1730
|
+
click.echo(f"Available: {', '.join(sections.keys())}")
|
|
958
1731
|
return
|
|
959
1732
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1733
|
+
# Print all sections
|
|
1734
|
+
click.echo(USAGE_HEADER)
|
|
1735
|
+
for content in sections.values():
|
|
1736
|
+
click.echo(content)
|
|
1737
|
+
click.echo()
|
|
963
1738
|
|
|
964
|
-
if global_install:
|
|
965
|
-
target_dir = Path.home() / ".claude" / "commands"
|
|
966
|
-
elif workspace_install:
|
|
967
|
-
target_dir = Path.cwd() / ".claude" / "commands"
|
|
968
|
-
else:
|
|
969
|
-
target_dir = Path.cwd() / ".claude" / "commands"
|
|
970
|
-
click.echo("Installing to workspace (.claude/commands/)")
|
|
971
|
-
|
|
972
|
-
if commands:
|
|
973
|
-
to_install = [c for c in commands if c in available]
|
|
974
|
-
not_found = [c for c in commands if c not in available]
|
|
975
|
-
if not_found:
|
|
976
|
-
click.echo(f"Warning: Commands not found: {', '.join(not_found)}", err=True)
|
|
977
|
-
else:
|
|
978
|
-
to_install = available
|
|
979
1739
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
return
|
|
1740
|
+
USAGE_HEADER = """
|
|
1741
|
+
# Ragtime Usage Guide
|
|
983
1742
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1743
|
+
Ragtime provides persistent memory for AI coding sessions. Use it via:
|
|
1744
|
+
- **MCP Server** (recommended for Claude Code / AI tools)
|
|
1745
|
+
- **CLI** (for scripts, hooks, and manual use)
|
|
1746
|
+
"""
|
|
1747
|
+
|
|
1748
|
+
USAGE_MCP = """
|
|
1749
|
+
## MCP Server Tools
|
|
1750
|
+
|
|
1751
|
+
After `ragtime init`, add to your `.mcp.json`:
|
|
1752
|
+
|
|
1753
|
+
```json
|
|
1754
|
+
{
|
|
1755
|
+
"mcpServers": {
|
|
1756
|
+
"ragtime": {
|
|
1757
|
+
"command": "ragtime",
|
|
1758
|
+
"args": ["serve"]
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
```
|
|
1763
|
+
|
|
1764
|
+
### Core Tools
|
|
1765
|
+
|
|
1766
|
+
| Tool | Purpose |
|
|
1767
|
+
|------|---------|
|
|
1768
|
+
| `mcp__ragtime__search` | Find relevant memories, docs, and code |
|
|
1769
|
+
| `mcp__ragtime__remember` | Store new knowledge |
|
|
1770
|
+
| `mcp__ragtime__list_memories` | Browse stored memories |
|
|
1771
|
+
| `mcp__ragtime__forget` | Delete a memory |
|
|
1772
|
+
| `mcp__ragtime__graduate` | Promote branch memory to app namespace |
|
|
1773
|
+
|
|
1774
|
+
### Search Examples
|
|
1775
|
+
|
|
1776
|
+
```
|
|
1777
|
+
# Find architecture knowledge
|
|
1778
|
+
mcp__ragtime__search(query="authentication flow", namespace="app")
|
|
1779
|
+
|
|
1780
|
+
# Find team conventions
|
|
1781
|
+
mcp__ragtime__search(query="error handling", namespace="team", type="convention")
|
|
1782
|
+
|
|
1783
|
+
# Search with auto-qualifier detection
|
|
1784
|
+
mcp__ragtime__search(query="how does auth work in mobile", tiered=true)
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
### Remember Examples
|
|
1788
|
+
|
|
1789
|
+
```
|
|
1790
|
+
# Store architecture insight
|
|
1791
|
+
mcp__ragtime__remember(
|
|
1792
|
+
content="JWT tokens expire after 15 minutes, refresh tokens after 7 days",
|
|
1793
|
+
namespace="app",
|
|
1794
|
+
type="architecture",
|
|
1795
|
+
component="auth"
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
# Store team convention
|
|
1799
|
+
mcp__ragtime__remember(
|
|
1800
|
+
content="Always use async/await, never .then() chains",
|
|
1801
|
+
namespace="team",
|
|
1802
|
+
type="convention"
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
# Store branch-specific decision
|
|
1806
|
+
mcp__ragtime__remember(
|
|
1807
|
+
content="Using Redis for session storage because of horizontal scaling needs",
|
|
1808
|
+
namespace="branch-feature/auth",
|
|
1809
|
+
type="decision"
|
|
1810
|
+
)
|
|
1811
|
+
```
|
|
1812
|
+
"""
|
|
1813
|
+
|
|
1814
|
+
USAGE_CLI = """
|
|
1815
|
+
## CLI Commands
|
|
1816
|
+
|
|
1817
|
+
### Memory Management
|
|
1818
|
+
|
|
1819
|
+
```bash
|
|
1820
|
+
# Search memories
|
|
1821
|
+
ragtime search "authentication patterns"
|
|
1822
|
+
ragtime search "conventions" --namespace team
|
|
1823
|
+
|
|
1824
|
+
# Add a convention
|
|
1825
|
+
ragtime add-convention "Always validate user input"
|
|
1826
|
+
ragtime add-convention -c auth "JWT must be validated on every request"
|
|
1827
|
+
|
|
1828
|
+
# List memories
|
|
1829
|
+
ragtime memories --namespace app
|
|
1830
|
+
ragtime memories --type convention
|
|
1831
|
+
|
|
1832
|
+
# Check conventions for changed files
|
|
1833
|
+
ragtime check-conventions
|
|
1834
|
+
ragtime check-conventions --all # For AI (comprehensive)
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
### Branch Context
|
|
1029
1838
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1839
|
+
```bash
|
|
1840
|
+
# Create branch context from GitHub issue
|
|
1841
|
+
ragtime new-branch 123 --branch feature/auth
|
|
1842
|
+
|
|
1843
|
+
# Graduate branch memories after PR merge
|
|
1844
|
+
ragtime graduate --branch feature/auth
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
### Indexing
|
|
1848
|
+
|
|
1849
|
+
```bash
|
|
1850
|
+
# Index docs and code
|
|
1851
|
+
ragtime index
|
|
1852
|
+
ragtime reindex # Full reindex
|
|
1853
|
+
```
|
|
1854
|
+
"""
|
|
1855
|
+
|
|
1856
|
+
USAGE_WORKFLOWS = """
|
|
1857
|
+
## Recommended Workflows
|
|
1858
|
+
|
|
1859
|
+
### Starting Work on an Issue
|
|
1860
|
+
|
|
1861
|
+
1. AI reads the issue and existing context:
|
|
1862
|
+
```
|
|
1863
|
+
mcp__ragtime__search(query="auth implementation", namespace="app")
|
|
1864
|
+
```
|
|
1865
|
+
|
|
1866
|
+
2. Check for branch context:
|
|
1867
|
+
```bash
|
|
1868
|
+
# Look for .ragtime/branches/{branch}/context.md
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
3. As you make decisions, store them:
|
|
1872
|
+
```
|
|
1873
|
+
mcp__ragtime__remember(
|
|
1874
|
+
content="Chose PKCE flow for mobile OAuth",
|
|
1875
|
+
namespace="branch-feature/oauth",
|
|
1876
|
+
type="decision"
|
|
1877
|
+
)
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
### Before Creating a PR
|
|
1881
|
+
|
|
1882
|
+
1. Check conventions:
|
|
1883
|
+
```bash
|
|
1884
|
+
ragtime check-conventions
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
2. Review branch memories for graduation candidates
|
|
1888
|
+
|
|
1889
|
+
### After PR Merge
|
|
1890
|
+
|
|
1891
|
+
1. Graduate valuable branch memories to app namespace:
|
|
1892
|
+
```bash
|
|
1893
|
+
ragtime graduate --branch feature/auth
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
### Session Handoff
|
|
1897
|
+
|
|
1898
|
+
Store context in `.ragtime/branches/{branch}/context.md`:
|
|
1899
|
+
- Current state
|
|
1900
|
+
- What's left to do
|
|
1901
|
+
- Key decisions made
|
|
1902
|
+
- Blockers or notes for next session
|
|
1903
|
+
"""
|
|
1904
|
+
|
|
1905
|
+
USAGE_CONVENTIONS = """
|
|
1906
|
+
## Convention System
|
|
1907
|
+
|
|
1908
|
+
### Storing Conventions
|
|
1909
|
+
|
|
1910
|
+
Conventions can live in:
|
|
1911
|
+
- **Files**: `.ragtime/CONVENTIONS.md` or `docs/conventions/`
|
|
1912
|
+
- **Doc sections**: Any `## Conventions` section in markdown
|
|
1913
|
+
- **Memories**: `team` namespace with type `convention`
|
|
1914
|
+
|
|
1915
|
+
```bash
|
|
1916
|
+
# Add via CLI (routes automatically)
|
|
1917
|
+
ragtime add-convention "Use snake_case for JSON fields"
|
|
1918
|
+
|
|
1919
|
+
# Add to specific file
|
|
1920
|
+
ragtime add-convention --to-file docs/api.md "Return 404 for missing resources"
|
|
1921
|
+
|
|
1922
|
+
# Add as memory only
|
|
1923
|
+
ragtime add-convention --to-memory "Prefer composition over inheritance"
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
### Convention Detection
|
|
1927
|
+
|
|
1928
|
+
The indexer automatically detects conventions from:
|
|
1929
|
+
- Files named `*conventions*`, `*rules*`, `*standards*`
|
|
1930
|
+
- Sections with headers like `## Conventions`, `## Rules`
|
|
1931
|
+
- Content between `<!-- convention -->` markers
|
|
1932
|
+
- Frontmatter: `has_conventions: true` or `convention_sections: [...]`
|
|
1933
|
+
|
|
1934
|
+
### Checking Conventions
|
|
1935
|
+
|
|
1936
|
+
```bash
|
|
1937
|
+
# Human use (filtered to relevant)
|
|
1938
|
+
ragtime check-conventions
|
|
1939
|
+
|
|
1940
|
+
# AI use (comprehensive)
|
|
1941
|
+
ragtime check-conventions --all --json
|
|
1942
|
+
```
|
|
1943
|
+
"""
|
|
1035
1944
|
|
|
1036
1945
|
|
|
1037
1946
|
@main.command("setup-ghp")
|
|
@@ -1066,7 +1975,8 @@ def setup_ghp(remove: bool):
|
|
|
1066
1975
|
return
|
|
1067
1976
|
|
|
1068
1977
|
# Updated path for .ragtime/
|
|
1069
|
-
|
|
1978
|
+
# NOTE: No quotes around template vars - GHP's shellEscape() handles escaping
|
|
1979
|
+
hook_command = "ragtime new-branch ${issue.number} --issue-json ${issue.json} --branch ${branch}"
|
|
1070
1980
|
|
|
1071
1981
|
result = subprocess.run(
|
|
1072
1982
|
[
|