diff --git a/CHANGELOG.md b/CHANGELOG.md index a05ff1a..e2be309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to okf-kit are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## 0.3.1 — 2026-07-09 + +### Added +- **`GET /api/books/{name}/toc` now includes each concept's `resource`** (its + original page URL). This lets a GUI reader map an in-bundle link to the concept + it points at and navigate in-app, instead of opening the browser for links to + pages that are already in the bundle. + ## 0.3.0 — 2026-07-08 ### Added @@ -189,6 +197,7 @@ okf chat docs-okf --provider ollama # chat offline, no key Install `[js]` in its own environment for now. Tracked in [#6](https://github.com/vinodborole/okf-kit/issues/6), fix planned for 0.1.1. +[0.3.1]: https://github.com/vinodborole/okf-kit/releases/tag/v0.3.1 [0.3.0]: https://github.com/vinodborole/okf-kit/releases/tag/v0.3.0 [0.2.0]: https://github.com/vinodborole/okf-kit/releases/tag/v0.2.0 [0.1.8]: https://github.com/vinodborole/okf-kit/releases/tag/v0.1.8 diff --git a/okf_kit/__init__.py b/okf_kit/__init__.py index 386f190..88e4304 100644 --- a/okf_kit/__init__.py +++ b/okf_kit/__init__.py @@ -1,3 +1,3 @@ """okf-kit — turn any website into a portable, agent-ready OKF bundle.""" -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/okf_kit/serve/reader.py b/okf_kit/serve/reader.py index 4bb8715..c4dbd0b 100644 --- a/okf_kit/serve/reader.py +++ b/okf_kit/serve/reader.py @@ -33,7 +33,11 @@ def ordered_concepts(bundle_dir) -> list[dict]: for p in files: cid = p.relative_to(root).as_posix()[:-3] fm, _ = _split_frontmatter(p.read_text(encoding="utf8")) - out.append({"id": cid, "title": fm.get("title") or cid.split("/")[-1]}) + out.append({ + "id": cid, + "title": fm.get("title") or cid.split("/")[-1], + "resource": fm.get("resource") or "", # original page URL — lets a GUI + }) # map in-bundle links to concepts return out @@ -51,7 +55,8 @@ def to_list(node: dict) -> list[dict]: items = [] for (kind, name), val in node.items(): if kind == "file": - items.append({"kind": "concept", "id": val["id"], "title": val["title"]}) + items.append({"kind": "concept", "id": val["id"], "title": val["title"], + "resource": val.get("resource", "")}) else: items.append({"kind": "section", "title": _prettify(name), "children": to_list(val)}) return items diff --git a/pyproject.toml b/pyproject.toml index f72ee37..1895b10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "okf-kit" -version = "0.3.0" +version = "0.3.1" description = "Turn any website into a portable, agent-ready Open Knowledge Format (OKF) bundle — build, sync, and chat, no LLM required to start." readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_serve.py b/tests/test_serve.py index 73d120c..51d2416 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -34,8 +34,8 @@ def client(tmp_path, monkeypatch): "---\ntype: Web Page\ntitle: Introduction\n---\n# Introduction\n\nWelcome.\n", encoding="utf8" ) (b / "pages" / "ownership.md").write_text( - "---\ntype: Web Page\ntitle: Ownership\n---\n# Ownership\n\n" - "## Borrowing\n\nOwnership means every value has exactly one owner.\n", + "---\ntype: Web Page\ntitle: Ownership\nresource: https://doc.rust-lang.org/book/ch04-01.html\n---\n" + "# Ownership\n\n## Borrowing\n\nOwnership means every value has exactly one owner.\n", encoding="utf8", ) (b / ".okf-kit").mkdir() @@ -72,6 +72,9 @@ def test_toc_and_concept(client): toc = client.get("/api/books/testbook/toc", headers=AUTH).json() ids = _concept_ids(toc) assert "pages/intro" in ids and "pages/ownership" in ids + # toc carries each concept's original URL so a GUI can map links → concepts + own = _find_concept(toc, "pages/ownership") + assert own["resource"] == "https://doc.rust-lang.org/book/ch04-01.html" c = client.get("/api/books/testbook/concept", params={"id": "pages/ownership"}, headers=AUTH).json() assert c["title"] == "Ownership" @@ -125,6 +128,17 @@ def _concept_ids(nodes): return out +def _find_concept(nodes, cid): + for n in nodes: + if n["kind"] == "concept" and n["id"] == cid: + return n + if n["kind"] == "section": + found = _find_concept(n["children"], cid) + if found: + return found + return None + + def _parse_sse(text): events = [] for block in text.strip().split("\n\n"):