Skip to content
Merged

Json #16

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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
![Tests](https://github.com/wr1/cfold/actions/workflows/tests.yml/badge.svg)![Version](https://img.shields.io/github/v/release/wr1/cfold)
# cfold

`cfold` is a command-line tool that helps you prepare codebases for interaction with Large Language Models (LLMs). It can "fold" a directory of code into a single JSON file and "unfold" a modified version back into a directory structure.
<!-- `cfold` is a command-line tool that helps you prepare codebases for interaction with Large Language Models (LLMs). It can `fold` a directory of code into a single JSON file and `unfold` a modified version back into a directory structure. -->

## Installation
- Fold files and instructions into json
- Unfold LLM return jsons in same format

```bash
pip install cfold
```

Or install from source with UV:
## Installation

```bash
uv pip install .
uv pip install https://github.com/wr1/cfold.git
```

## Usage
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cfold"
version = "0.2.1"
version = "0.2.2"
description = "A command line tool to fold and unfold Python projects for LLM interaction"
authors = [{ name = "wr1", email = "8971152+wr1@users.noreply.github.com" }]
requires-python = "~=3.10"
Expand Down
3 changes: 2 additions & 1 deletion src/cfold/cli/fold.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def fold(ctx, files, output, prompt, dialect):
try:
with open(output, "w", encoding="utf-8") as outfile:
json.dump(
data.model_dump(exclude={"instructions": {"__all__": {"synopsis"}}}),
data.model_dump(),
outfile,
indent=2,
)
Expand Down Expand Up @@ -120,3 +120,4 @@ def fold(ctx, files, output, prompt, dialect):
console.print(
f"Codebase folded into [cyan]{output}[/cyan] and content [green]copied to clipboard[/green]."
)

3 changes: 2 additions & 1 deletion src/cfold/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def init(ctx, output, custom, dialect):
data.instructions.append(Instruction(type="user", content=custom, name="custom"))
with open(output, "w", encoding="utf-8") as outfile:
json.dump(
data.model_dump(exclude={"instructions": {"__all__": {"synopsis"}}}),
data.model_dump(),
outfile,
indent=2,
)
Expand All @@ -58,3 +58,4 @@ def init(ctx, output, custom, dialect):




1 change: 1 addition & 0 deletions src/cfold/cli/unfold.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,4 @@ def unfold(foldfile, original_dir, output_dir):




11 changes: 6 additions & 5 deletions src/cfold/resources/prompts.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
common:
instructions:
- type: assistant
synopsis: "define output format for LLM"
synopsis: "define cfold output format"
content: |-
This JSON file represents a project codebase in cfold format, with prompts for LLM.
The fields are 'instructions' (list of instruction objects), 'files'.
Each instruction: {type: 'system'|'user'|'assistant', content: string, name: string (optional)}
Each instruction: {type: 'system'|'user'|'assistant', content: string, name: string (optional), synopsis: string (optional)}
Each file in 'files': {path: string, content: Optional[string], delete: bool (default: false)}
To update the codebase, modify the 'files' array as per the following rules. Do not modify 'instructions' unless explicitly specified.
- Folding: 'cfold fold <files> -o <output.json>' captures specified files into this .json.
Expand All @@ -16,8 +16,8 @@ common:
- To move/rename a file: Add a delete object for the old path ('delete': true) and a new object with the new 'path', full 'content', and 'delete': false.
- Only include modified, new, or deleted files in the 'files' array; unchanged files are preserved from the original directory (if provided with -i).
- Always provide full file content for additions and modifications; no partial updates.
- Supports .foldignore file with gitignore-style patterns to exclude files during folding (directory mode).
- Paths are relative to the current working directory (CWD) by default.
- Supports .foldrc YAML file for custom dialects, patterns, and instructions, which can reference defaults via 'pre'.
- Write output as a full dict {'files': [...] }, not the bare 'files' array.

default:
Expand Down Expand Up @@ -66,8 +66,8 @@ py:
included_suffix:
- ".py"
- ".toml"
included_dirs:
- "src"
# included_dirs:
# - "src"

pytest:
pre: [common, default]
Expand Down Expand Up @@ -105,3 +105,4 @@ typst:
- ".typ"



1 change: 1 addition & 0 deletions src/cfold/utils/foldignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,4 @@ def should_include_file(




47 changes: 35 additions & 12 deletions src/cfold/utils/instructions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Load instructions and patterns for specified dialect."""

from importlib import resources
from pathlib import Path
import yaml
from typing import List, Dict
from typing import List, Dict, Optional
from cfold.models import Instruction


Expand Down Expand Up @@ -39,21 +40,32 @@ def collect_instructions(
return instructions


def load_instructions(dialect: str = "default") -> tuple[List[Instruction], Dict]:
def load_instructions(dialect: str = "default", directory: Optional[Path] = None) -> tuple[List[Instruction], Dict]:
"""Load the boilerplate instructions and patterns for the specified dialect from prompts.yaml as a list of Instruction."""
if directory is None:
directory = Path.cwd()
local_config = {}
local_path = directory / ".foldrc"
if local_path.exists():
with local_path.open("r", encoding="utf-8") as f:
local_config = yaml.safe_load(f) or {}

try:
with resources.files("cfold").joinpath("resources/prompts.yaml").open(
"r", encoding="utf-8"
) as f:
config = yaml.safe_load(f) # Use safe_load for security
default_config = yaml.safe_load(f) # Use safe_load for security
except Exception as e:
raise RuntimeError(
f"Failed to load instructions for dialect '{dialect}' from prompts.yaml: {e}"
f"Failed to load default instructions: {e}"
)
if dialect not in config:
raise ValueError(f"Dialect '{dialect}' not found in prompts.yaml")
instructions_list = collect_instructions(config, dialect)
instr = config[dialect]

combined_config = {**default_config, **local_config}

if dialect not in combined_config:
raise ValueError(f"Dialect '{dialect}' not found in combined configurations")
instructions_list = collect_instructions(combined_config, dialect)
instr = combined_config[dialect]
patterns = {
"included": [
f"*{pat}" for pat in instr.get("included_suffix", [])
Expand All @@ -64,17 +76,28 @@ def load_instructions(dialect: str = "default") -> tuple[List[Instruction], Dict
return instructions_list, patterns


def get_available_dialects() -> List[str]:
"""Get the list of available dialects from prompts.yaml."""
def get_available_dialects(directory: Optional[Path] = None) -> List[str]:
"""Get the list of available dialects from prompts.yaml and .foldrc."""
if directory is None:
directory = Path.cwd()
local_config = {}
local_path = directory / ".foldrc"
if local_path.exists():
with local_path.open("r", encoding="utf-8") as f:
local_config = yaml.safe_load(f) or {}

try:
with resources.files("cfold").joinpath("resources/prompts.yaml").open(
"r", encoding="utf-8"
) as f:
config = yaml.safe_load(f)
return [k for k in config.keys() if k != "common"]
default_config = yaml.safe_load(f)
except Exception as e:
raise RuntimeError(f"Failed to load dialects: {e}")

combined_config = {**default_config, **local_config}
return [k for k in combined_config.keys() if k != "common"]