# Loopy: an in-memory, zero-side-effect filesystem API over a single Python string.

Loopy is a filesystem sandbox whose entire state lives in a single string. It comes with a bash‑style shell so you or your agent can navigate, search, and manipulate a tree‑structured knowledge base with composable commands.

The idea is to simulate a file system to build and navigate a knowledge tree with POSIX-style commands that agents are already familiar with. 


```python
from loopy import Loopy
from loopy.shell import run

raw = "<root><concepts><ml><supervised><classification>predicts categories</classification></supervised></ml></concepts></root>"
tree = Loopy(raw)
# tree.raw == "<root><concepts><ml><supervised><classification>predicts categories</classification></supervised></ml></concepts></root>"
# tree.tree("/") ==
# root/
# └── concepts/
#     └── ml/
#         └── supervised/
#             └── classification: predicts categories

output = run("cd /concepts/ml/supervised && cat classification", tree)
# output == "predicts categories"

run('touch /concepts/ml/supervised/regression "predicts continuous values"', tree)

serialized = tree.raw
# serialized == "<root><concepts><ml><supervised><classification>predicts categories</classification><regression>predicts continuous values</regression></supervised></ml></concepts></root>"
# tree.tree("/") ==
# root/
# └── concepts/
#     └── ml/
#         └── supervised/
#             ├── classification: predicts categories
#             └── regression: predicts continuous values
```

## Why?
When an agent processes information, it needs somewhere to put it - somewhere it can search, reorganize, and grow organically. For any type of knowledge base like agent memories, product taxonomies, etc the challenge is to expose CRUD type interactions without a pile of specialized tools (search, create, delete, etc.) that are added to context.

Recursive Language Models (RLMs) introduced the idea of putting the entire context into a Python variable and let the model recursively interact with it, instead of reasoning over everything in one shot. RLMs: https://alexzhang13.github.io/blog/2025/rlm/. I really liked it, but enabling a python REPL seemed like a bad tradeoff for generality. 

Loopy imposes a known structure (a tree / filesystem), and replaces the python REPL with a bash syntax over a string. Agents are RL'd in filesystem-like setups, and this provides a sandboxed way to give an agent access to a knowledge base without touching the OS filesystem.

Why this approach:

- simple - a single string can represent the full data
- known structure - stored in a file system format agents already know and love
- composition - compose search commands to quickly navigate the data


## How it works

Loopy keeps an in-memory node tree and exposes filesystem-like operations. There is no OS filesystem I/O; all mutations stay in memory until you serialize. The raw string is generated on demand and can be parsed back into the same structure.

```
<root><concepts><ml><supervised>...</supervised></ml></concepts></root>
```

```python
from loopy import Loopy

tree = (
    Loopy()
    .mkdir("/concepts/ml/supervised", parents=True)
    .touch("/concepts/ml/supervised/regression", "predicts continuous values")
    .touch("/concepts/ml/supervised/classification", "predicts categories")
    .mkdir("/concepts/ml/unsupervised")
    .touch("/concepts/ml/unsupervised/clustering", "groups similar items")
)

tree.grep("predicts", content=True)  # find concepts by description
tree.find("/concepts", type="f")  # all leaf concepts
tree.ls("/concepts/ml")  # ['supervised', 'unsupervised']

# tree.raw
# <root><concepts><ml><supervised><regression>predicts continuous values</regression><classification>predicts categories</classification></supervised><unsupervised><clustering>groups similar items</clustering></unsupervised></ml></concepts></root>
```

### File-backed

```python
from loopy import FileBackedLoopy, load, save

tree = FileBackedLoopy("notes.loopy")
tree.touch("/ideas/mcp", "Expose shell with MCP")  # auto-saved

tree = load("notes.loopy")
tree.mkdir("/scratch", parents=True)
save(tree, "notes.loopy")  # explicit save for non-file-backed trees
```

## Install

```bash
# Local install
uv pip install -e .

# PyPI
uv pip install loopy-fs
```

## API

### Core Operations

| Method | Description |
|--------|-------------|
| `mkdir(path, parents=True)` | Create directory (category) |
| `touch(path, content)` | Create file (entity) with content |
| `cat(path)` | Read content |
| `ls(path)` | List children |
| `rm(path, recursive=True)` | Delete node |
| `mv(src, dst)` | Move/rename |
| `cp(src, dst)` | Copy |
| `ln(target, link)` | Create symlink |
| `exists(path)` | Check existence |

### Search

| Method | Description |
|--------|-------------|
| `grep(pattern, content=True)` | Search by regex (returns paths) |
| `grep(pattern, lines=True)` | Search content line-by-line (returns `path:lineno:line`) |
| `find(path, type="f")` | Find by type (f=file, d=dir, l=link) |
| `glob(pattern)` | Glob patterns (`**/*.py`) |

### Navigation

| Method | Description |
|--------|-------------|
| `cd(path)` | Change directory |
| `.cwd` | Current directory |
| `..` support | `tree.ls("..")` works |

### Inspection

| Method | Description |
|--------|-------------|
| `tree(path)` | Pretty print hierarchy |
| `du(path)` | Count nodes |
| `info(path)` | Metadata dict |
| `isdir(path)` / `isfile(path)` | Type checks |
| `islink(path)` | Check if symlink |
| `readlink(path)` | Get symlink target |
| `backlinks(path)` | Find all symlinks pointing to path |
| `walk(path)` | os.walk() style |
| `.raw` | The underlying string |

### Utilities

| Function | Description |
|----------|-------------|
| `slugify(text)` | Convert any string to a valid path segment |

```python
from loopy import slugify

slugify("Hello World!")      # -> "hello-world"
slugify("My File (2).txt")  # -> "my-file-2-.txt"
slugify("café résumé")      # -> "café-résumé"
```

Path segments only allow alphanumeric characters, underscores, hyphens, and dots. `slugify` converts anything else into a safe form.

All mutating operations return `self` for chaining.

## Shell

Loopy includes a bash-style command runner designed for agents. Use `run()` to navigate and compose operations directly against the in-memory tree.

```python
from loopy import Loopy
from loopy.shell import run

tree = (
    Loopy()
    .mkdir("/concepts/ml/supervised", parents=True)
    .touch("/concepts/ml/supervised/classification", "predicts categories")
)

output = run(
    "cd /concepts/ml && ls | grep supervised && cat supervised/classification",
    tree,
)
```

Start a REPL loop from a file-backed database (auto-saves on every mutation):

```bash
uv run python -m loopy.shell notes.loopy
```

Start a REPL loop with a sample database:

```bash
uv run python -m loopy.shell examples/product_catalog.loopy
```

Quick shell example:

```text
loopy> ls -R sports
sports:
fitness/
outdoor/

loopy> find /sports -type f
/sports/fitness/weights/dumbbells_set
/sports/fitness/cardio/yoga_mat
/sports/outdoor/camping/tent_4person
/sports/outdoor/hiking/backpack_40L
```

### Shell Commands

| Command | Description |
|---------|-------------|
| `ls [path] [-R]` | List directory contents |
| `cd <path>` | Change directory |
| `pwd` | Print working directory |
| `cat [path] [--range start len]` | Show file contents |
| `head [path] [-n N]` | Show first N lines (default 10) |
| `tail [path] [-n N]` | Show last N lines (default 10) |
| `wc [-lwc] [path]` | Count lines/words/chars |
| `sort [-rnu] [path]` | Sort lines |
| `tree [path]` | Show tree structure |
| `find [path] [-name pat] [-type d\|f\|l]` | Find files/directories/symlinks |
| `grep <pat> [path] [-i] [-v] [-c] [-n]` | Search by regex (`-n` for line matches) |
| `du [path] [-c]` | Count nodes or content size |
| `info [path]` | Show node metadata |
| `touch <path> [content]` | Create file |
| `write <path> [content]` | Write to file (overwrites) |
| `mkdir [-p] <path>` | Create directory |
| `rm [-r] <path>` | Remove file/directory |
| `mv <src> <dst>` | Move/rename |
| `cp <src> <dst>` | Copy |
| `ln <target> <link>` | Create symlink |
| `readlink <path>` | Show symlink target |
| `sed <path> <pat> <repl> [-i] [-r]` | Search and replace |
| `split <delim> [path]` | Split content by delimiter |
| `echo <text>` | Print text |
| `printf <fmt> [args]` | Print formatted text |
| `help` | Show help |

### Command Chaining

| Operator | Description |
|----------|-------------|
| `cmd1 \| cmd2` | Pipe output of cmd1 to cmd2 |
| `cmd1 ; cmd2` | Run both, continue on failure |
| `cmd1 && cmd2` | Run cmd2 only if cmd1 succeeds |
| `cmd1 \|\| cmd2` | Run cmd2 only if cmd1 fails |

## Example Databases

Loopy ships with example databases in `examples/`:

- `product_catalog.loopy` - E-commerce taxonomy
- `knowledge_graph.loopy` - ML/CS concept ontology
- `bookmarks.loopy` - Browser bookmarks
- `recipes.loopy` - Recipe collection
- `org_chart.loopy` - Company org chart

```bash
uv run python -m loopy.shell examples/knowledge_graph.loopy
loopy> cat /concepts/ml/deep_learning/architectures/transformer
def:Attention-based architecture|prereq:attention,mlp|paper:attention_is_all_you_need

loopy> grep "prereq:.*transformer" -c
2

loopy> grep -n "price:.*99" /clothing
/clothing/mens/shoes/loafers_brown:1:type:formal|price:89.99|color:brown
```

Or load in Python:

```python
from loopy.file_store import load

tree = load("examples/product_catalog.loopy")
tree.ls("/clothing/mens/shoes")  # ['loafers_brown']
tree.grep("price:.*99", content=True)  # products ending in .99
```

## License

MIT
