Skip to content

Commit b0de6b8

Browse files
author
jzhu
committed
add
1 parent 19f7bb5 commit b0de6b8

2 files changed

Lines changed: 158 additions & 45 deletions

File tree

code_assistant_manager/cli/prompts_commands.py

Lines changed: 111 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import typer
99

1010
from code_assistant_manager.menu.base import Colors
11-
from code_assistant_manager.prompts import PROMPT_FILE_PATHS, Prompt, PromptManager
11+
from code_assistant_manager.prompts import (
12+
PROMPT_FILE_PATHS,
13+
Prompt,
14+
PromptManager,
15+
get_prompt_file_path,
16+
)
1217

1318
logger = logging.getLogger(__name__)
1419

@@ -19,6 +24,7 @@
1924

2025
# Valid app types
2126
VALID_APP_TYPES = ["claude", "codex", "gemini"]
27+
VALID_LEVELS = ["user", "project"]
2228

2329

2430
def _get_prompt_manager() -> PromptManager:
@@ -264,8 +270,20 @@ def sync_prompts(
264270
"-a",
265271
help="App type to sync to (claude, codex, gemini). Required if prompt_id is specified.",
266272
),
273+
level: str = typer.Option(
274+
"user",
275+
"--level",
276+
"-l",
277+
help="Sync level: 'user' (~/.claude/) or 'project' (current directory)",
278+
),
267279
):
268280
"""Sync prompts to app files. Can sync a specific prompt or all active prompts."""
281+
if level not in VALID_LEVELS:
282+
typer.echo(
283+
f"{Colors.RED}✗ Invalid level: {level}. Valid: {', '.join(VALID_LEVELS)}{Colors.RESET}"
284+
)
285+
raise typer.Exit(1)
286+
269287
manager = _get_prompt_manager()
270288

271289
if prompt_id:
@@ -287,21 +305,37 @@ def sync_prompts(
287305
typer.echo(f"{Colors.RED}✗ Prompt not found: {prompt_id}{Colors.RESET}")
288306
raise typer.Exit(1)
289307

308+
file_path = get_prompt_file_path(app_type, level)
309+
if not file_path:
310+
typer.echo(f"{Colors.RED}✗ Unknown app type: {app_type}{Colors.RESET}")
311+
raise typer.Exit(1)
312+
290313
try:
291-
manager._sync_prompt_to_file(prompt.content, app_type)
292-
typer.echo(f"{Colors.GREEN}✓ Synced '{prompt_id}' to {app_type}{Colors.RESET}")
293-
file_path = PROMPT_FILE_PATHS.get(app_type)
314+
# Ensure parent directory exists
315+
file_path.parent.mkdir(parents=True, exist_ok=True)
316+
file_path.write_text(prompt.content, encoding="utf-8")
317+
typer.echo(
318+
f"{Colors.GREEN}✓ Synced '{prompt_id}' to {app_type} ({level}){Colors.RESET}"
319+
)
294320
typer.echo(f" {Colors.CYAN}File:{Colors.RESET} {file_path}")
295321
except Exception as e:
296322
typer.echo(f"{Colors.RED}✗ Error: {e}{Colors.RESET}")
297323
raise typer.Exit(1)
298324
else:
299325
# Sync all active prompts
326+
if level == "project":
327+
typer.echo(
328+
f"{Colors.RED}✗ --level project requires specifying a prompt ID{Colors.RESET}"
329+
)
330+
raise typer.Exit(1)
331+
300332
results = manager.sync_all()
301333

302334
for app, synced_prompt_id in results.items():
303335
if synced_prompt_id:
304-
typer.echo(f"{Colors.GREEN}{app}: synced ({synced_prompt_id}){Colors.RESET}")
336+
typer.echo(
337+
f"{Colors.GREEN}{app}: synced ({synced_prompt_id}){Colors.RESET}"
338+
)
305339
else:
306340
typer.echo(f"{Colors.YELLOW}{app}: no active prompt{Colors.RESET}")
307341

@@ -409,6 +443,12 @@ def unsync_prompt(
409443
"-a",
410444
help="App type to unsync (claude, codex, gemini)",
411445
),
446+
level: str = typer.Option(
447+
"user",
448+
"--level",
449+
"-l",
450+
help="Unsync level: 'user' (~/.claude/) or 'project' (current directory)",
451+
),
412452
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
413453
):
414454
"""Clear/unsync the prompt file for an app (removes CLAUDE.md, AGENTS.md, or GEMINI.md content)."""
@@ -418,13 +458,21 @@ def unsync_prompt(
418458
)
419459
raise typer.Exit(1)
420460

421-
file_path = PROMPT_FILE_PATHS.get(app_type)
461+
if level not in VALID_LEVELS:
462+
typer.echo(
463+
f"{Colors.RED}✗ Invalid level: {level}. Valid: {', '.join(VALID_LEVELS)}{Colors.RESET}"
464+
)
465+
raise typer.Exit(1)
466+
467+
file_path = get_prompt_file_path(app_type, level)
422468
if not file_path:
423469
typer.echo(f"{Colors.RED}✗ Unknown app type: {app_type}{Colors.RESET}")
424470
raise typer.Exit(1)
425471

426472
if not file_path.exists():
427-
typer.echo(f"{Colors.YELLOW}Prompt file does not exist: {file_path}{Colors.RESET}")
473+
typer.echo(
474+
f"{Colors.YELLOW}Prompt file does not exist: {file_path}{Colors.RESET}"
475+
)
428476
return
429477

430478
if not force:
@@ -434,53 +482,74 @@ def unsync_prompt(
434482
file_path.write_text("", encoding="utf-8")
435483
typer.echo(f"{Colors.GREEN}✓ Cleared prompt file: {file_path}{Colors.RESET}")
436484

437-
# Deactivate any active prompt for this app
438-
manager = _get_prompt_manager()
439-
prompts = manager.get_all()
440-
for prompt in prompts.values():
441-
if prompt.enabled and prompt.app_type == app_type:
442-
prompt.enabled = False
443-
manager.update(prompt)
444-
typer.echo(f" {Colors.CYAN}Deactivated:{Colors.RESET} {prompt.id}")
485+
# Deactivate any active prompt for this app (only for user level)
486+
if level == "user":
487+
manager = _get_prompt_manager()
488+
prompts = manager.get_all()
489+
for prompt in prompts.values():
490+
if prompt.enabled and prompt.app_type == app_type:
491+
prompt.enabled = False
492+
manager.update(prompt)
493+
typer.echo(f" {Colors.CYAN}Deactivated:{Colors.RESET} {prompt.id}")
445494
except Exception as e:
446495
typer.echo(f"{Colors.RED}✗ Error: {e}{Colors.RESET}")
447496
raise typer.Exit(1)
448497

449498

450499
@prompt_app.command("status")
451-
def show_prompt_status():
500+
def show_prompt_status(
501+
level: str = typer.Option(
502+
"all",
503+
"--level",
504+
"-l",
505+
help="Show status for: 'user', 'project', or 'all' (default)",
506+
),
507+
):
452508
"""Show prompt status for all apps."""
453509
manager = _get_prompt_manager()
454510

455-
typer.echo(f"\n{Colors.BOLD}Prompt Status:{Colors.RESET}\n")
511+
levels_to_show = ["user", "project"] if level == "all" else [level]
456512

457-
for app_type in VALID_APP_TYPES:
458-
file_path = PROMPT_FILE_PATHS.get(app_type)
459-
active_prompt = manager.get_active_prompt(app_type)
460-
461-
typer.echo(f"{Colors.BOLD}{app_type.capitalize()}:{Colors.RESET}")
462-
typer.echo(f" {Colors.CYAN}File:{Colors.RESET} {file_path}")
463-
464-
if file_path and file_path.exists():
465-
content = file_path.read_text(encoding="utf-8")
466-
if content.strip():
467-
lines = content.strip().split("\n")
468-
preview = lines[0][:50] + "..." if len(lines[0]) > 50 else lines[0]
469-
typer.echo(f" {Colors.GREEN}Content:{Colors.RESET} {preview}")
470-
typer.echo(f" {Colors.CYAN}Lines:{Colors.RESET} {len(lines)}")
471-
else:
472-
typer.echo(f" {Colors.YELLOW}Content:{Colors.RESET} (empty)")
473-
else:
474-
typer.echo(f" {Colors.YELLOW}Content:{Colors.RESET} (file not found)")
513+
if level != "all" and level not in VALID_LEVELS:
514+
typer.echo(
515+
f"{Colors.RED}✗ Invalid level: {level}. Valid: user, project, all{Colors.RESET}"
516+
)
517+
raise typer.Exit(1)
475518

476-
if active_prompt:
477-
typer.echo(
478-
f" {Colors.GREEN}Active Prompt:{Colors.RESET} {active_prompt.name} ({active_prompt.id})"
479-
)
480-
else:
481-
typer.echo(f" {Colors.YELLOW}Active Prompt:{Colors.RESET} None")
519+
for lvl in levels_to_show:
520+
typer.echo(
521+
f"\n{Colors.BOLD}Prompt Status ({lvl.capitalize()} Level):{Colors.RESET}\n"
522+
)
482523

483-
typer.echo()
524+
for app_type in VALID_APP_TYPES:
525+
file_path = get_prompt_file_path(app_type, lvl)
526+
527+
typer.echo(f"{Colors.BOLD}{app_type.capitalize()}:{Colors.RESET}")
528+
typer.echo(f" {Colors.CYAN}File:{Colors.RESET} {file_path}")
529+
530+
if file_path and file_path.exists():
531+
content = file_path.read_text(encoding="utf-8")
532+
if content.strip():
533+
lines = content.strip().split("\n")
534+
preview = lines[0][:50] + "..." if len(lines[0]) > 50 else lines[0]
535+
typer.echo(f" {Colors.GREEN}Content:{Colors.RESET} {preview}")
536+
typer.echo(f" {Colors.CYAN}Lines:{Colors.RESET} {len(lines)}")
537+
else:
538+
typer.echo(f" {Colors.YELLOW}Content:{Colors.RESET} (empty)")
539+
else:
540+
typer.echo(f" {Colors.YELLOW}Content:{Colors.RESET} (file not found)")
541+
542+
# Only show active prompt for user level
543+
if lvl == "user":
544+
active_prompt = manager.get_active_prompt(app_type)
545+
if active_prompt:
546+
typer.echo(
547+
f" {Colors.GREEN}Active Prompt:{Colors.RESET} {active_prompt.name} ({active_prompt.id})"
548+
)
549+
else:
550+
typer.echo(f" {Colors.YELLOW}Active Prompt:{Colors.RESET} None")
551+
552+
typer.echo()
484553

485554

486555
# Add list shorthand

code_assistant_manager/prompts.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,72 @@
11
"""Prompt management for Code Assistant Manager.
22
33
This module provides functionality to manage prompts for AI coding assistants.
4-
Prompts can be synced to the actual tool config files:
4+
Prompts can be synced to the actual tool config files at user or project level:
5+
6+
User level (default):
57
- Claude: ~/.claude/CLAUDE.md
68
- Codex: ~/.codex/AGENTS.md
79
- Gemini: ~/.gemini/GEMINI.md
10+
11+
Project level (current directory):
12+
- Claude: ./CLAUDE.md
13+
- Codex: ./AGENTS.md
14+
- Gemini: ./GEMINI.md
815
"""
916

1017
import json
1118
import logging
19+
import os
1220
import shutil
1321
from datetime import datetime
1422
from pathlib import Path
1523
from typing import Dict, List, Optional
1624

1725
logger = logging.getLogger(__name__)
1826

19-
# Prompt file paths for each app type
20-
PROMPT_FILE_PATHS = {
27+
# User-level prompt file paths for each app type
28+
USER_PROMPT_FILE_PATHS = {
2129
"claude": Path.home() / ".claude" / "CLAUDE.md",
2230
"codex": Path.home() / ".codex" / "AGENTS.md",
2331
"gemini": Path.home() / ".gemini" / "GEMINI.md",
2432
}
2533

34+
# Project-level prompt file names for each app type
35+
PROJECT_PROMPT_FILE_NAMES = {
36+
"claude": "CLAUDE.md",
37+
"codex": "AGENTS.md",
38+
"gemini": "GEMINI.md",
39+
}
40+
41+
# Keep for backward compatibility
42+
PROMPT_FILE_PATHS = USER_PROMPT_FILE_PATHS
43+
44+
45+
def get_prompt_file_path(
46+
app_type: str, level: str = "user", project_dir: Optional[Path] = None
47+
) -> Optional[Path]:
48+
"""
49+
Get the prompt file path for an app type at the specified level.
50+
51+
Args:
52+
app_type: The app type (claude, codex, gemini)
53+
level: Either "user" or "project"
54+
project_dir: Project directory (defaults to current working directory)
55+
56+
Returns:
57+
Path to the prompt file, or None if invalid
58+
"""
59+
if level == "user":
60+
return USER_PROMPT_FILE_PATHS.get(app_type)
61+
elif level == "project":
62+
filename = PROJECT_PROMPT_FILE_NAMES.get(app_type)
63+
if not filename:
64+
return None
65+
if project_dir is None:
66+
project_dir = Path.cwd()
67+
return project_dir / filename
68+
return None
69+
2670

2771
class Prompt:
2872
"""Represents a prompt configuration."""

0 commit comments

Comments
 (0)