ki2-externai 0.3.1
Manage read-only external projects for AI coding assistants.
Requires Python: >=3.10

# ki2-externai

> 🔒 Manage read-only external projects for AI coding assistants (Codex, Copilot, etc.)

`ki2-externai` is a tiny command-line tool that lets you **mount external libraries or frameworks in read-only mode** inside your project.  
It’s especially useful when working with code-assistant tools (e.g. OpenAI Codex) that need to _see_ your internal frameworks, but should **never modify them**.

## 🧠 Why?

When developing with AI assistants, sometimes you want the assistant to explore your internal codebases (frameworks, utils, etc.) to understand project context — but you **don’t want it to modify** them.
`ki2-externai` makes this safe and transparent:

- Every external library is _visible_ inside your project,
- but _physically read-only_ thanks to `mount --bind` + `remount,ro`,
- and easily reversible.

Works great in Linux and WSL2.

## 🚀 Quick start

### 1. Install

```bash
# Recommended for CLI tools (isolated, global command)
pipx install ki2-externai

# Alternative (inside an existing Python environment)
pip install ki2-externai
```

### 2. Register projects once (from inside them)

```bash
cd /path/to/libA
externai register libA
```

This stores a name → absolute path mapping in your OS data dir (e.g.
`~/.local/share/ki2/externai/registry.json`). If you omit a name, the folder name
is used.

### 3. Link externals inside your project (CLI creates config)

```bash
cd /path/to/your-project
externai add libA
```

If `externai.json` is missing, `externai add` creates a minimal config before
appending the entry.

### 4. Mount everything read-only

```bash
externai mount
```

This creates one folder per import under `.externals/` (e.g. `.externals/libA`).
Each entry is a **bind mount** that is then **remounted read-only**.

### 5. Check status (or unmount)

```bash
externai status
```

Output example:

```
name                 src_exists mounted    mount_source                             ntfs-ish?
-----------------------------------------------------------------------------------------------
libA                 yes        mounted    /home/adrien/dev/libA                    no
```

```bash
externai unmount
```

## ⚙️ Commands

| Command               | Description                                           |
| --------------------- | ----------------------------------------------------- |
| `externai mount`      | Mount all configured externals in read-only mode      |
| `externai unmount`    | Unmount all externals                                 |
| `externai status`     | Show status table                                     |
| `externai doctor`     | Run system checks (detect NTFS / missing tools)       |
| `externai register`   | Register the current directory (optional custom name) |
| `externai unregister` | Remove a registry entry (by name or current folder)   |
| `externai registry`   | List every registered name and its source path        |
| `externai add`        | Add an import by name or path                         |
| `externai remove`     | Remove an import from `externai.json`                 |

## 🧾 Config file (optional)

You can edit `externai.json` manually if you want, but you don’t have to.
The CLI commands above are enough for most workflows.

Example config (package.json‑like):

```json
{
  "version": 1,
  "externalsDir": ".externals",
  "imports": [
    "libA",
    { "name": "frameworkX", "path": "/home/adrien/dev/frameworkX" },
    { "name": "toolkit", "mount": "tk" }
  ]
}
```

Config semantics:

- `version` (number, optional): schema version. Default `1`.
- `externalsDir` (string, optional): destination folder for mounts. Default `.externals`.
- `imports` (array, required, can be empty): list of projects to import. Each entry can be:
  - a string: the registered project `name` (its `path` is resolved from the registry),
  - or an object with at least `name` and optional fields:
    - `path` (string, optional): source path on disk (if omitted, resolved from registry),
    - `mount` (string, optional): name of the subfolder under `externalsDir` (defaults to `name`).

## 🧾 Registry details

- Run `externai register [custom-name]` inside the folder you want to expose. The
  command stores an absolute path → name mapping in your OS data dir (e.g.
  `~/.local/share/ki2/externai/registry.json`). If you omit the name, the
  current folder name is used.
- `externai register <name>` refuses to overwrite existing entries or duplicate
  paths unless you confirm. Registering the exact same name/path is a no-op.
- To remove an entry, run `externai unregister <name>` or simply
  `externai unregister` inside the project root to drop any entry pointing to the
  current path.
- Use `externai registry` to list all registered names and their directories. You
  can then reference these names directly inside `externai.json` imports.
- Example config relying on the registry only:

  ```json
  {
    "imports": ["libA", "frameworkX"]
  }
  ```

  All paths are injected dynamically at mount time.

## 🧪 Tests

- `uv run pytest` (ensure the `tests` optional dependency is installed via `uv sync`)

## ➕ Add/remove imports

- `externai add <name>` adds the named project to `externai.json` using a registered path.
  If the name is missing from the registry, `externai` explains how to register it or how to
  pass an explicit path (see below).
- `externai add <name> <path>` creates an object import for the provided path (will fail if
  the path does not exist).
- When `externai.json` is absent, `externai add` creates a minimal config before appending the entry.
- `externai remove <name>` drops the import entry with the given name.

The commands operate on the `externai.json` file at the project root, so you must run them
from within your project (the same directory that contains the config).

## 💡 Tips

- Best results on WSL ext4 filesystem (not `/mnt/c`).
- Requires `mount`, `umount`, and optionally `findmnt`.
- No root install: `sudo` is used only for mount operations.

## 📦 Project structure

```
your-project/
├── externai.json          # config file
├── .externals/
│   ├── libA/               # bind-mounted (read-only)
│   └── frameworkX/
└── ki2_externai/
    ├── __init__.py
    ├── cli.py
    ├── config.py
    ├── operations.py
    └── utils.py
```

`pip install ki2-externai` exposes the `externai` console script (defined in `pyproject.toml`), so you can run `externai mount` directly.

## 🪪 License

MIT License.