Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions src/shelfai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
chunk Run heuristic pre-filter on a monolithic agent file
tokens Show token counts and budget checks for chunk bundles
abstract Generate heuristic chunk abstracts and routing context
merge Merge multiple chunks into one file
split Split a large chunk into smaller pieces
dead-chunks Identify chunks never loaded at runtime
suggest Analyse an AGENT.md and recommend a chunking strategy
keywords List and inspect chunk classification keywords
Expand Down Expand Up @@ -1765,6 +1767,146 @@ def abstract_cmd(
console.print(f"\n[green]Stored abstracts in {store.store_path}[/green]")


# ──────────────────────────────────────────────
# shelfai merge
# ──────────────────────────────────────────────


@app.command("merge")
def merge_cmd(
chunk_ids: list[str] = typer.Argument(..., help="Chunk files or IDs to merge"),
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file name"),
preview: bool = typer.Option(False, "--preview", help="Preview merged output without writing"),
delete_originals: bool = typer.Option(False, "--delete-originals", help="Delete source chunks after merge"),
separator: str = typer.Option("\n\n---\n\n", "--separator", help="Separator inserted between merged chunks"),
shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
"""Merge multiple chunks into one file."""
from shelfai.core.merge_split import ChunkMerger
from shelfai.core.shelf import Shelf

shelf = Shelf(shelf_path)
if not shelf.exists():
console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
raise typer.Exit(1)

merger = ChunkMerger()

if preview:
try:
preview_text = merger.preview_merge(shelf_path, chunk_ids)
except Exception as exc:
console.print(f"[red]Merge preview failed: {exc}[/red]")
raise typer.Exit(1)
if not preview_text:
console.print("[yellow]Nothing to preview.[/yellow]")
return
console.print("\n[bold]Merge Preview[/bold]\n")
console.print(preview_text)
console.print()
return

if not output:
console.print("[red]`--output` is required unless you use `--preview`.[/red]")
raise typer.Exit(1)

result = merger.merge(
shelf_path=shelf_path,
chunk_ids=chunk_ids,
output_name=output,
delete_originals=delete_originals,
separator=separator,
)

if not result.success:
console.print(f"[red]{result.message}[/red]")
raise typer.Exit(1)

console.print(f"[green]{result.message}[/green]")
console.print(f"[dim]Source chunks: {', '.join(result.source_chunks)}[/dim]")
console.print(f"[dim]Estimated tokens: ~{result.total_tokens:,}[/dim]")
console.print()


# ──────────────────────────────────────────────
# shelfai split
# ──────────────────────────────────────────────


@app.command("split")
def split_cmd(
chunk_id: str = typer.Argument(..., help="Chunk file or ID to split"),
by: str = typer.Option("heading", "--by", help="Split method: heading or tokens"),
level: int = typer.Option(2, "--level", help="Heading level to split on"),
max_tokens: int = typer.Option(3000, "--max", help="Max tokens per chunk when splitting by tokens"),
preview: bool = typer.Option(False, "--preview", help="Preview split plan without writing"),
delete_original: bool = typer.Option(False, "--delete-original", help="Delete the source chunk after splitting"),
shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
"""Split a large chunk into smaller pieces."""
from shelfai.core.merge_split import ChunkSplitter
from shelfai.core.shelf import Shelf

shelf = Shelf(shelf_path)
if not shelf.exists():
console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
raise typer.Exit(1)

splitter = ChunkSplitter()

if preview:
try:
preview_items = splitter.preview_split(
shelf_path=shelf_path,
chunk_id=chunk_id,
method=by,
heading_level=level,
max_tokens=max_tokens,
)
except Exception as exc:
console.print(f"[red]Split preview failed: {exc}[/red]")
raise typer.Exit(1)

if not preview_items:
console.print("[yellow]Nothing to preview.[/yellow]")
return

console.print("\n[bold]Split Preview[/bold]\n")
for item in preview_items:
console.print(f" [cyan]{item['name']}[/cyan] (~{item['tokens']:,} tokens)")
console.print(f" [dim]{item['preview']}[/dim]")
console.print()
return

if by not in {"heading", "tokens"}:
console.print("[red]`--by` must be `heading` or `tokens`.[/red]")
raise typer.Exit(1)

if by == "heading":
result = splitter.split_by_heading(
shelf_path=shelf_path,
chunk_id=chunk_id,
heading_level=level,
delete_original=delete_original,
)
else:
result = splitter.split_by_tokens(
shelf_path=shelf_path,
chunk_id=chunk_id,
max_tokens=max_tokens,
delete_original=delete_original,
)

if not result.success:
console.print(f"[red]{result.message}[/red]")
raise typer.Exit(1)

console.print(f"[green]{result.message}[/green]")
for output_file, tokens in zip(result.output_files, result.tokens_per_chunk):
console.print(f" [cyan]{output_file}[/cyan] (~{tokens:,} tokens)")
console.print()


# ──────────────────────────────────────────────
# shelfai suggest
# ──────────────────────────────────────────────
Expand Down
Loading
Loading