# Development Guide

Step-by-step circuit for developing `pi-obsidian-cli` and testing it against a live Obsidian vault.

## Prerequisites

- Node.js >= 22.19.0
- Obsidian 1.13+ with CLI enabled: **Settings → General → Advanced → Command line interface**
- `obsidian` binary on your `PATH`

## One-time setup

```bash
git clone https://github.com/frNNcs/pi-obsidian-cli
cd pi-obsidian-cli
npm ci
```

## Circuit: edit → check → test

### 1. Edit source

Source files are TypeScript loaded directly by pi via [jiti](https://github.com/unjs/jiti) — **no build step needed**.

| File | Role |
|------|------|
| `index.ts` | Extension entrypoint (tools, commands, events). Audited fixed-script wrappers use a separate policy path; user-supplied DataviewJS is opt-in and confirmed. `executeCommand` is the common policy/confirmation path for tools, slash commands, and the TUI browser. It also rejects `open`/`tab:open` for `.svg` files and uses a short 8s timeout for those commands. |
| `config.ts` | `obsidianCli` settings section loader |
| `catalog.ts` | `__completions` discovery |
| `runner.ts` | CLI exec orchestration, warmup, queue |
| `render.ts` | Output formatting, TSV alignment |
| `interactive.ts` | TUI browser |

### 2. Type-check

```bash
npm run check
# or: npm run typecheck
```

This runs `tsc --noEmit` with strict mode. Must pass before committing.

### 3. Validate packaging

```bash
npm run pack:dry
```

Ensures all `files` declared in `package.json` actually exist, `node_modules` is not bundled, and the tarball would be well-formed.

### 4. Run automated tests

```bash
npm test
```

This runs the 30-case Excalidraw extraction harness plus the policy suite covering permissions, dangerous commands, DataviewJS opt-in, Tasks naming, browser routing, export payloads, and portable process detection.

### 5. Test with pi (ad-hoc entrypoint)

The fastest way to test your changes in isolation — loads **only** this extension, skipping any globally installed copy:

```bash
pi --no-extensions -e ./index.ts
```

Then in the pi session:
```
/obsidian          # open TUI browser
obsidian read path=Notes/hello.md
obsidian search query=mysearch
/reload            # reload after new changes
```

> **Important:** `--no-extensions` ensures no duplicate tools from a previously installed copy.

### 5. Install from local path (persistent)

When you want the extension to persist across pi sessions without copying files, run this **from your terminal** (not inside a pi session):

```bash
# Project-local: writes/updates .pi/settings.json in the current directory.
# The local path is referenced as-is — no copy, no symlink.
pi install -l /absolute/path/to/pi-obsidian-cli
```

Your edits take effect after `/reload` — no reinstall needed. If pi asks for trust, use `/trust` in a pi session or pass `--approve` on the CLI.

To uninstall (project-local):
```bash
pi remove -l /absolute/path/to/pi-obsidian-cli
```

To uninstall (global):
```bash
pi remove /absolute/path/to/pi-obsidian-cli
```

### 6. Global install from local path

```bash
pi install /absolute/path/to/pi-obsidian-cli
```

This writes the local path into pi's global `settings.json`. The path is referenced as-is — files are neither copied nor symlinked. As with `-l`, changes take effect after `/reload`.

## Acceptance criteria

Before submitting a PR, ensure:

- [ ] `npm run check` passes (zero errors)
- [ ] `npm run pack:dry` lists the expected runtime files, README, license, preview, development guide, and rendered/editable diagram assets; tests and internal discovery notes stay out of the tarball
- [ ] Extension loads without errors: `pi --no-extensions -e ./index.ts`
- [ ] `/obsidian` TUI opens and shows the command catalog
- [ ] At least one read-only tool works end-to-end (e.g., `obsidian_files`)
- [ ] Permission gating blocks disallowed commands with a clear explanation
- [ ] No duplicate tools in `/tools` output (check with `--no-extensions` first)

## Excalidraw export validation

`obsidian_excalidraw_export` reads Excalidraw scene JSON from several file formats and **persists the output file inside the vault** alongside the source drawing. The parser recognizes the native Obsidian `.excalidraw.md` structure — frontmatter, warning block, headings, `%%` comments, the `## Drawing` heading and its JSON code fence — while keeping support for the legacy pure-JSON and simple-markdown formats.

**Output path derivation** follows these rules (format is `svg` or `png`):
- `foo.excalidraw.md` → `foo.svg` / `foo.png`
- `foo.mindmap.md` → `foo.mindmap.svg` / `foo.mindmap.png`
- `foo.excalidraw` → `foo.svg` / `foo.png`
- Fallback: appends `.svg` / `.png` to the source path

**The tool never overwrites existing files.** If the derived output path already exists, it returns `success: false` with `Output file already exists: <path>`.

SVG files are persisted via `app.vault.create(outputPath, svgString)`. PNG files are decoded from base64 to a `Uint8Array` and persisted via `app.vault.createBinary(outputPath, buffer)`.

Successful results include `data` (the raw SVG string or PNG base64, for backward compatibility), `outputPath` (the vault path of the saved file), and `bytes` (the file size).

The extraction pipeline tries three strategies in order:

![Excalidraw extraction pipeline](extraction-pipeline.svg)

The editable source is [`extraction-pipeline.excalidraw`](extraction-pipeline.excalidraw). A failed match continues to the next strategy; compressed or invalid scenes return a descriptive error, while valid scenes are parsed, rendered through a temporary file, and persisted as SVG or PNG.

Strategy 3 scans every top-level brace pair (string- and escape-aware), validates each candidate with `JSON.parse`, and keeps the one whose `type === "excalidraw"` — it is deliberately **not** a greedy regex, so a random JSON object before the real drawing cannot hijack the match.

### Local regression harness

```bash
npm run test:excalidraw
```

Runs `test-extraction.mjs`, a self-contained local harness (no vault, no Obsidian) with **30 cases** (21 extraction + 9 path-derivation) covering:

- **Strategy 1** — native `.excalidraw.md` (`## Drawing` + fence, with/without `json` tag, CRLF endings);
- **Strategy 2** — pure JSON, frontmatter + JSON, frontmatter + fenced JSON, compact single-line JSON;
- **Strategy 3** — JSON embedded in markdown, the real `.excalidraw.md` variant with `===` delimiters wrapping embedded JSON (covered by the balanced extraction), multiple JSON objects picking the Excalidraw one, nested braces, brace-looking strings, escaped quotes, backslashes, and the anti-greedy trap;
- compressed detection, `not-found` / invalid / empty-element error paths.

> `test-extraction.mjs` stays **out of the npm tarball** — it is excluded through the `files` whitelist in `package.json` — but it remains part of the repo as a runnable regression suite, wired to the `test:excalidraw` script.

### Test-case matrix

| Case | Input file | Expected result |
|------|------------|-----------------|
| Native uncompressed | `.excalidraw.md` with frontmatter, warning, headings, `%%`, `## Drawing` + JSON fence | Exports SVG/PNG |
| Balanced-brace real variant | `.excalidraw.md` with `===` delimiters wrapping embedded scene JSON (no fence) | Exports SVG/PNG |
| Pure JSON | `.json` / `.excalidraw.md` containing only the scene JSON (no frontmatter, no fence) | Exports SVG/PNG |
| Simple markdown | YAML frontmatter + JSON (optionally fenced) at the start | Exports SVG/PNG |
| Compressed (`compressed-json`) | frontmatter `excalidraw-plugin: parsed` | Descriptive error: decompress the file in Obsidian first |
| Empty scene | `elements: []` | Error "No elements found" |

### Smoke test

```bash
npm run check
npm run test:excalidraw
pi --no-extensions -e ./index.ts
```

Inside the pi session:

```
obsidian read path=Notes/sample.excalidraw.md
obsidian_excalidraw_export file=Notes/sample.excalidraw.md
obsidian_excalidraw_export file=Notes/sample.excalidraw.md format=png
```

> Real validation requires a live environment: Obsidian with the Excalidraw plugin installed and enabled, `allowFixedScripts` enabled, and a live vault containing the test drawings. Static checks (`npm run check`, `npm run pack:dry`) and the local harness (`npm run test:excalidraw`) verify type, packaging, and extraction logic only — they do not exercise the exporter against a live vault.

## Project structure decisions

### `.pi/` is gitignored

The `.pi/` directory is generated locally by `pi install -l <path>` and contains vault-specific settings. It is **not versioned** because each developer has their own vault path, permission preferences, and Obsidian CLI binary location.

If you need shared project-level pi configuration (e.g., model, agent personality), place it in a separate tracked config file and reference it from your local `.pi/settings.json`.

### No build step

Pi uses `jiti` to load TypeScript extensions directly. There is no `dist/` or compilation step. The `tsc --noEmit` check is purely for type safety.

### Zero runtime dependencies

All pi packages (`pi-ai`, `pi-agent-core`, `pi-coding-agent`, `pi-tui`) and `typebox` are declared as `peerDependencies`. They are provided by the pi runtime at execution time. They appear in `devDependencies` only so that TypeScript can resolve types during `tsc --noEmit`.

## CI pipeline

The CI workflow (`.github/workflows/ci.yml`) runs on every push/PR to `main`:

1. `npm ci` — reproducible install from lockfile
2. `npm run typecheck` — strict TypeScript check
3. `npm run pack:dry` — packaging integrity validation
4. `npm test` — Excalidraw extraction and policy regression suites
5. `npm run metrics:gate` — safety and package-shape invariants

## Release checklist

1. [ ] `npm run check` passes on `main`
2. [ ] `npm run pack:dry` shows correct file list
3. [ ] Manual smoke test: `pi --no-extensions -e ./index.ts` works
4. [ ] Update `version` in `package.json` following semver
5. [ ] Commit and tag: `git tag vX.Y.Z`
6. [ ] Push: `git push origin main --tags`
7. [ ] Install from git to verify: `pi install git:github.com/frNNcs/pi-obsidian-cli`
8. [ ] (Optional) Publish to npm: `npm publish`
9. [ ] (Optional) Install from npm: `pi install npm:pi-obsidian-cli`
