Metadata-Version: 2.5
Name: vault2okf
Version: 0.1.0
Summary: Validate Open Knowledge Format (OKF) bundles and convert Obsidian vaults to OKF.
Project-URL: Homepage, https://github.com/abhishekacharya200/vault2okf
Project-URL: Issues, https://github.com/abhishekacharya200/vault2okf/issues
Project-URL: Specification, https://github.com/GoogleCloudPlatform/open-knowledge-format
Author: Abhishek Acharya
License-Expression: MIT
License-File: LICENSE
Keywords: agents,cli,knowledge-base,llm,markdown,obsidian,okf,open-knowledge-format,validator
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Text Processing :: Markup :: Markdown
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: markdown-it-py>=3
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6
Requires-Dist: rich>=13
Requires-Dist: tomli>=2; python_version < '3.11'
Requires-Dist: typer>=0.12
Description-Content-Type: text/markdown

# vault2okf

[![CI](https://github.com/abhishekacharya200/vault2okf/actions/workflows/ci.yml/badge.svg)](https://github.com/abhishekacharya200/vault2okf/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/vault2okf.svg)](https://pypi.org/project/vault2okf/)
[![Python](https://img.shields.io/pypi/pyversions/vault2okf.svg)](https://pypi.org/project/vault2okf/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/abhishekacharya200/vault2okf/blob/main/LICENSE)

Convert an Obsidian vault into an [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/open-knowledge-format) bundle, then check and repair any OKF bundle before an agent or a teammate reads it.

![vault2okf converting a vault, then catching a broken link and a note without frontmatter](https://raw.githubusercontent.com/abhishekacharya200/vault2okf/main/docs/demo.gif)

OKF is a folder of markdown files with YAML frontmatter, where each file is one concept and concepts link to each other with normal markdown links. An Obsidian vault is close to that, but its `[[wikilinks]]` only work inside Obsidian and its notes carry no `type`. vault2okf closes the gap and then keeps the bundle healthy.

- **`vault2okf convert`** turns a vault into a conformant bundle: wikilinks become real links, embedded files are copied, frontmatter and a per-folder `index.md` are written. The vault is never modified, and re-runs change only what changed.
- **`vault2okf validate`** checks any bundle against the OKF v0.2 spec: frontmatter, reserved files, trust and lifecycle fields, broken links and heading anchors, leftover wikilinks, orphan notes, and file names that break on Windows or macOS. It prints plain text, JSON or GitHub annotations.
- **`vault2okf fix`** repairs the common problems in place: missing `type`, `title` and `description`, tags written as one string, wikilinks, an unquoted `okf_version`.
- **`vault2okf init`**, **`schema`** and **`rules`** scaffold a bundle, print a JSON Schema for concept frontmatter, and list every rule.

It also works as a Python library, a GitHub Action and a pre-commit hook. Settings can live in the repository, in `vault2okf.toml` or `pyproject.toml`.

## Install

```bash
pipx install vault2okf      # or: uv tool install vault2okf
```

Needs Python 3.10 or newer.

## Quick start

```bash
vault2okf convert ~/Documents/MyVault ./knowledge
vault2okf validate ./knowledge
```

```text
$ vault2okf validate knowledge

Home.md
    L13  warning OKF030 link target not found: /Runbooks/deploy.md

todo.md
         warning OKF031 no document in the bundle links to this concept
     L1  error   OKF001 file must start with a `---` YAML block

not conformant: 4 concepts, 10 links, 1 error, 2 warnings
```

Exit codes: `0` conformant, `1` validation failed, `2` usage error.

## Why vault2okf

[okf-cli](https://pypi.org/project/okf-cli/) also converts markdown to OKF. It is built for plain markdown folders and adds commands to browse a bundle. vault2okf is built for Obsidian vaults and for catching and fixing problems. Both tools were run in September 2026 on the vault in [`examples/vault`](https://github.com/abhishekacharya200/vault2okf/tree/main/examples/vault) and on a test bundle with eight planted problems:

| | vault2okf 0.1.0 | okf-cli 0.6.1 |
|---|---|---|
| `[[wikilinks]]`, aliases, heading links, `![[embeds]]` | converted to markdown links | left unchanged |
| Embedded images and files | copied into the bundle | not copied |
| Frontmatter, `type` and `index.md` checks | yes | yes |
| Timestamp, `status` and `generated` checks | yes | no |
| Broken links, leftover wikilinks, orphans, `log.md` dates | reported | not reported |
| Planted problems reported (out of 8) | 8 | 1 |
| Repairs problems in place | yes (`fix`) | no |
| JSON and GitHub annotation output | yes | no |
| `list` and `read` commands, generated `AGENTS.md` | no | yes |

The two install different commands (`vault2okf` and `okf`), so they can sit side by side.

The validator finds no errors in the four sample bundles in Google's [OKF repository](https://github.com/GoogleCloudPlatform/open-knowledge-format/tree/main/bundles). One of them, `stackoverflow`, gets eight warnings because its tags are written as one string instead of a list; `vault2okf fix` rewrites exactly those eight lines.

## What `convert` does

| In the vault | In the bundle |
|---|---|
| `[[Note]]`, `[[folder/Note]]` | `[Note](/folder/Note.md)`, resolved the way Obsidian does (full path, then unique name, then closest folder) |
| `[[Note\|alias]]`, `[[Note#Heading]]`, `[[#Heading]]` | `[alias](/Note.md)`, `[Note > Heading](/Note.md#heading)`, `[Heading](#heading)` |
| `[[ship]]` where no note is named `ship` but one lists it in `aliases:` | a link to that note, listed in the report (Obsidian itself leaves such links unresolved) |
| `![[image.png]]` | `![image](/image.png)`, with the file copied into the bundle |
| `![[Other note]]` | a normal link to the note |
| `[[missing]]` | plain text, listed in the report |
| `%% comments %%` | removed, because Obsidian never shows them and they often hold private notes; `--keep-comments` keeps them |
| `tags:` and inline `#tags` | one `tags` list; anything inside code, links, HTML or comments is ignored |
| no `type` | `Note`, or set one with `--type-map` or `--type-from-folder` |
| first `# Heading` | `title`, falling back to the file name |
| first paragraph | `description`, one sentence of at most 200 characters; notes without a paragraph use their first callout, list item or section headings |
| file modified time | `generated: { by: vault2okf/x.y.z, at: ... }` |
| other frontmatter (`aliases`, `cssclasses`, ...) | kept as is |
| notes named `index.md` or `log.md` | renamed `index-note.md` / `log-note.md`, since OKF reserves those names |
| names Windows rejects (`What? Is: this.md`, `CON.md`) or that differ only by case | renamed as little as possible (`What Is- this.md`, `CON_.md`, `case (2).md`) with every link updated |
| `.obsidian/`, hidden files, symbolic links | skipped |

It also writes an `index.md` in every folder (notes grouped by `type`, with `okf_version: "0.2"` in the root one) and a root `log.md`. Every rename, alias link and unresolved link is listed in the report.

```bash
vault2okf convert vault out \
  --type-map 'Daily/*=Daily Note' \
  --type-map 'Runbooks/*=Runbook' \
  --exclude 'Private/*' \
  --attachments referenced      # referenced (default) | all | none
```

### Updating a bundle

Run the same command again with `--force` after the vault changes:

```text
$ vault2okf convert vault knowledge --force
updated knowledge: 1 concept added, 2 changed, 0 removed, 41 unchanged
```

Only files whose content changed are written, so unchanged files keep their timestamps and a git diff shows just the real changes. `generated.at` moves only when a note's content changes, not when a fresh checkout resets file times. `log.md` keeps its history and gains an entry per change, newest first:

```markdown
## 2026-09-26
* **Creation**: Added [Kubernetes](Hosting/Kubernetes.md).
* **Update**: Changed [Vercel](Hosting/Vercel.md).
* **Removal**: Removed `Hosting/Heroku.md`, which is no longer in the vault.
```

Safety: `convert` never follows symbolic links, so no file from outside the vault can end up in the bundle. It refuses to write inside the vault or into a non-empty folder it did not create, and it only ever removes files listed in its own `.vault2okf.json` manifest, after the new files are written. `--dry-run` reports without writing anything.

## What `validate` checks

`vault2okf rules` prints the full table. Severity follows the spec: things a bundle **must** do are errors, things consumers **must tolerate** are warnings.

| Code | Severity | Meaning |
|---|---|---|
| OKF001 | error | Concept has no frontmatter block |
| OKF002 | error | Frontmatter is not valid YAML |
| OKF003 | error | `type` missing, empty or not a string |
| OKF004 | error | Frontmatter is not a mapping |
| OKF010 | error | `index.md` carries frontmatter other than a root `okf_version` |
| OKF020 | error | `log.md` date heading is not `YYYY-MM-DD` |
| OKF040 | error | Malformed `generated`, `verified`, `sources`, `status`, `stale_after` or attested-computation fields, or a timestamp without a UTC offset |
| OKF030 | warning | Broken link (the spec says consumers tolerate this) |
| OKF031 | warning | Orphan concept, nothing links to it |
| OKF033 | warning | Leftover `[[wikilink]]` |
| OKF034 | warning | File or folder name Windows rejects, or two names that differ only by case |
| OKF035 | warning | Link to a `#heading` that does not exist in its target (GitHub's anchor rules) |
| OKF041 / OKF042 | warning | Missing recommended `title` / `description` |
| OKF043 | warning | `generated.by` or `verified[].by` is not `<producer>/<version>`, `human:<id>` or `process:<id>` |
| OKF044 | warning | Recommended field of the wrong type, such as tags written as one string |
| OKF050 | warning | `stale_after` has passed |

A concept containing only `type` is fully conformant, and unknown types and keys are never errors.

```bash
vault2okf validate bundle --strict               # warnings fail the run too
vault2okf validate bundle -i OKF031 -i OKF042    # skip rules
vault2okf validate bundle --format json          # machine readable
vault2okf validate bundle --format github        # inline annotations in Actions
vault2okf validate bundle -x 'drafts/*'          # skip paths
```

## Fix a bundle

`vault2okf fix` works on any OKF bundle, whoever produced it, and changes files in place. Commit first, or preview with `--dry-run`.

```text
$ vault2okf fix knowledge
Home.md: set type to 'Note', added title, added description, converted 1 wikilink
Hosting/Vercel.md: added description, wrote tags as a list, converted 1 wikilink
fixed 7 problems in 2 files
left for you:
  Hosting/Vercel.md: [[Docker]] matches no file in the bundle

validation

Hosting/Vercel.md
     L7  warning OKF033 [[Docker]] is a wikilink; use [text](/path.md)

conformant: 2 concepts, 2 links, 0 errors, 1 warning
```

It adds a missing `type` (`--default-type`, `Note` unless set), `title` (first heading, else the file name) and `description` (first sentence), writes string tags as a list, turns numbers into text where text belongs, converts wikilinks that match a file in the bundle, and quotes an unquoted `okf_version`. Only the lines that change are rewritten, so YAML comments, other keys and the file's line endings are kept. Anything that needs a person, like invalid YAML or a link to a note that does not exist, is listed instead of guessed.

## Settings file

Put defaults in `vault2okf.toml`, or under `[tool.vault2okf]` in `pyproject.toml`. The nearest one at or above the current folder is used, and flags on the command line always win. `--config` picks a file by path.

```toml
[convert]
type-from-folder = true
type-map = { "Daily/*" = "Daily Note", "Runbooks/*" = "Runbook" }
exclude = ["Private/*"]
attachments = "referenced"      # referenced | all | none
keep-comments = false

[validate]
strict = true
ignore = ["OKF031"]
exclude = ["drafts/*"]

[fix]
default-type = "Note"
```

In `pyproject.toml` the sections become `[tool.vault2okf.convert]`, `[tool.vault2okf.validate]` and `[tool.vault2okf.fix]`. Unknown sections, settings and rule codes are errors, so a typo never passes silently.

## GitHub Action

```yaml
- uses: actions/checkout@v4
- uses: abhishekacharya200/vault2okf@v0.1.0
  with:
    path: knowledge
    strict: "true"
```

## pre-commit

```yaml
repos:
  - repo: https://github.com/abhishekacharya200/vault2okf
    rev: v0.1.0
    hooks:
      - id: okf-validate
        args: [knowledge]
```

## Library

```python
from vault2okf import ConvertOptions, convert_vault, fix_bundle, validate_bundle

convert_vault("MyVault", "knowledge", ConvertOptions(type_from_folder=True))
fix_bundle("knowledge")

report = validate_bundle("knowledge", ignore=["OKF031"])
for d in report.diagnostics:
    print(d.code, d.path, d.line, d.message)
```

`vault2okf schema > okf-frontmatter.schema.json` gives editors a JSON Schema for concept frontmatter.

## Known limits

- Indented (4-space) code blocks are not detected by the converter, so a `[[link]]` inside one is still converted.
- Block references (`[[Note#^id]]`) lose the block part and link to the note.
- Dataview, Templater and other plugin syntax is copied as is.
- `fix` cannot repair frontmatter that is not valid YAML; it lists those files instead.

## Development

```bash
uv sync
uv run pytest --cov=vault2okf
uv run ruff check . && uv run ruff format --check . && uv run mypy src
```

See [CONTRIBUTING.md](https://github.com/abhishekacharya200/vault2okf/blob/main/CONTRIBUTING.md). This project is not affiliated with Google. OKF is an open specification published by the Google Cloud Data Cloud team.

## License

[MIT](https://github.com/abhishekacharya200/vault2okf/blob/main/LICENSE)
