okf 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +78 -33
- data/lib/okf/cli.rb +30 -0
- data/lib/okf/server/app.rb +30 -0
- data/lib/okf/server/graph/template.html.erb +17 -7
- data/lib/okf/server/graph.rb +25 -9
- data/lib/okf/skill/SKILL.md +11 -15
- data/lib/okf/skill/reference/cli.md +19 -2
- data/lib/okf/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45d6619cf9144fbaf4026e51ad238213e916c6796ffd1199d189d6b64c47352e
|
|
4
|
+
data.tar.gz: c26b86f1b2173feefd02159a2dd280f93a4e8ee915eb077ef49558fced6c8ac3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 254038bdb6383fe94882793c6b2d2433917536793670d23fad1be4c2678e9a8c042164c997ba8ae64950ced447ca850e4263bb1eee9ab2230b33b2b83f8efd94
|
|
7
|
+
data.tar.gz: 1ada5ef52a4848d340619a1ccf10bc35ddbae7504c3e53cc447f824e44f90d145579a024ae17c15c38eb84cc0592353b20967adda709c631a38a298161e6f99f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-07-15
|
|
4
|
+
|
|
5
|
+
- New CLI verb: `okf render <dir> [-o FILE]` — the live graph as one static,
|
|
6
|
+
self-contained HTML file, so it hosts where a server can't (GitHub Pages, an
|
|
7
|
+
object store, an attachment). It is the same page `okf server` serves, one
|
|
8
|
+
switch apart: the browser's five on-demand reads — bodies, descriptions,
|
|
9
|
+
catalog, index, logs — now route through named getters that resolve from an
|
|
10
|
+
injected `EMBED` payload instead of the network, so the whole bundle rides
|
|
11
|
+
inside the file with no server and no build step. Prints to stdout (`okf
|
|
12
|
+
render docs > public/index.html`) or writes `-o FILE`. The embedded data is
|
|
13
|
+
`</script>`-escaped exactly like the boot payload and every body still renders
|
|
14
|
+
through `DOMPurify.sanitize(marked.parse(...))`, so the trust boundary holds;
|
|
15
|
+
the trade-off is weight — each body is inlined, so a big bundle makes a big
|
|
16
|
+
file, and `okf server` stays the choice at scale.
|
|
17
|
+
- Official Docker image: `ghcr.io/serradura/okf`, a portable CLI that runs every
|
|
18
|
+
`okf` command (the graph server included) with no Ruby on the host. It is built
|
|
19
|
+
from source and published multi-arch (`linux/amd64`, `linux/arm64`) to the
|
|
20
|
+
GitHub Container Registry on each release tag, so the image always matches the
|
|
21
|
+
gem. Mount a bundle at `/data`; for `server`, add `--bind 0.0.0.0` and publish
|
|
22
|
+
`-p 8808:8808`. See the README's Docker section.
|
|
23
|
+
|
|
3
24
|
## [1.5.0] - 2026-07-13
|
|
4
25
|
|
|
5
26
|
- New CLI verb: `okf search <dir> <term…>` — deterministic ranked retrieval
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://okfgem.com">
|
|
3
3
|
<picture>
|
|
4
4
|
<source media="(prefers-color-scheme: dark)" srcset=".github/hero-dark.png">
|
|
5
|
-
<img src=".github/hero-light.png" width="100%" alt="okf-gem:
|
|
5
|
+
<img src=".github/hero-light.png" width="100%" alt="okf-gem: the complete harness for the Open Knowledge Format. An Agent Skill, a CLI and library, and a Graph let your agent author, curate, and consume your project's knowledge. 100% local.">
|
|
6
6
|
</picture>
|
|
7
7
|
</a>
|
|
8
8
|
</p>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<p align="center">
|
|
11
11
|
<a href="https://rubygems.org/gems/okf"><img src="https://img.shields.io/gem/v/okf" alt="Gem version"></a>
|
|
12
12
|
<a href="https://rubygems.org/gems/okf"><img src="https://img.shields.io/gem/dt/okf" alt="Downloads"></a>
|
|
13
|
+
<a href="https://github.com/serradura/okf-gem/pkgs/container/okf"><img src="https://img.shields.io/badge/ghcr.io-okf-2496ED?logo=docker&logoColor=white" alt="Docker image"></a>
|
|
13
14
|
<a href="https://github.com/serradura/okf-gem/actions/workflows/main.yml"><img src="https://github.com/serradura/okf-gem/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
14
15
|
<a href="https://github.com/serradura/okf-gem"><img src="https://img.shields.io/badge/ruby-%3E%3D%202.4-black" alt="Ruby >= 2.4"></a>
|
|
15
16
|
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License: Apache-2.0"></a>
|
|
@@ -21,18 +22,20 @@
|
|
|
21
22
|
<b><a href="https://okfgem.com">Site</a></b> ·
|
|
22
23
|
<b><a href="https://okfgem.com/docs/">Docs</a></b> ·
|
|
23
24
|
<b><a href="https://demo.okfgem.com">Live demo</a></b> ·
|
|
24
|
-
<b><a href="https://claude.okfgem.com">Claude plugin</a></b>
|
|
25
|
+
<b><a href="https://claude.okfgem.com">Claude plugin</a></b> ·
|
|
26
|
+
<b><a href="https://docker.okfgem.com">Docker image</a></b>
|
|
25
27
|
</p>
|
|
26
28
|
|
|
27
|
-
**okf-gem** (`okf` on RubyGems) is the complete
|
|
28
|
-
**Open Knowledge Format (OKF)** v0.1 bundles
|
|
29
|
+
**okf-gem** (`okf` on RubyGems) is the complete harness for
|
|
30
|
+
**Open Knowledge Format (OKF)** v0.1 bundles: create, maintain, and consume
|
|
31
|
+
your project's knowledge with your agent. The package is **Agent Skill + CLI/Lib + Graph**: an agent skill that authors and curates, a CLI and Ruby library that validate, lint, search, and embed, and a graph to explore, live or static, in one gem that runs 100% local. A bundle is a directory of Markdown files with YAML frontmatter that humans and agents read from one source; each file is a _concept_. The gem does not define a new place to keep knowledge; it gives you leverage over knowledge that already lives as Markdown.
|
|
29
32
|
|
|
30
33
|
The package, end to end:
|
|
31
34
|
|
|
32
35
|
<p align="center">
|
|
33
36
|
<picture>
|
|
34
37
|
<source media="(prefers-color-scheme: dark)" srcset=".github/overview-dark.png">
|
|
35
|
-
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the
|
|
38
|
+
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the Graph, in three modes: okf server (a live local server), okf render (the same page exported as one static, self-contained HTML file you can host anywhere), and OKF::Server::App (the Rack app mounted in a Rails route). One gem, 100% local, Ruby 2.4 or newer, only rack and webrick as dependencies.">
|
|
36
39
|
</picture>
|
|
37
40
|
</p>
|
|
38
41
|
|
|
@@ -41,13 +44,13 @@ command-line tool (the library API is also usable in-process). Each capability
|
|
|
41
44
|
below links to the concept that documents it: this gem's own knowledge is an OKF
|
|
42
45
|
bundle, so you can read its design in the format it defends.
|
|
43
46
|
|
|
44
|
-
| Capability
|
|
45
|
-
|
|
|
46
|
-
| [Companion agent skill](.okf/capabilities/agent-skill.md)
|
|
47
|
-
| [Conformance validator](.okf/capabilities/validator.md)
|
|
48
|
-
| [Curation linter](.okf/capabilities/linter.md)
|
|
49
|
-
| [Interactive graph
|
|
50
|
-
| [Library API](.okf/capabilities/library-api.md)
|
|
47
|
+
| Capability | What it answers | Verb |
|
|
48
|
+
| --------------------------------------------------------- | --------------------------------- | ------------------- |
|
|
49
|
+
| [Companion agent skill](.okf/capabilities/agent-skill.md) | Can an agent author it? | `skill` |
|
|
50
|
+
| [Conformance validator](.okf/capabilities/validator.md) | Is this a legal OKF bundle? (§9) | `validate` |
|
|
51
|
+
| [Curation linter](.okf/capabilities/linter.md) | Is it navigable, complete, fresh? | `lint` / `loose` |
|
|
52
|
+
| [Interactive graph](.okf/capabilities/graph-server.md) | Explore it — live or static? | `server` / `render` |
|
|
53
|
+
| [Library API](.okf/capabilities/library-api.md) | Can my Ruby program use it? | in-process |
|
|
51
54
|
|
|
52
55
|
> [!TIP]
|
|
53
56
|
> **Browse the gem as knowledge, not just docs.** This README is the front door;
|
|
@@ -85,18 +88,21 @@ work with it.
|
|
|
85
88
|
Knowledge already has several homes near an agent, and each holds a different
|
|
86
89
|
thing. None of the others is built for curated, durable team knowledge:
|
|
87
90
|
|
|
88
|
-
| | OKF bundle (this)
|
|
89
|
-
| ------------------------------ |
|
|
90
|
-
| Holds | curated team knowledge
|
|
91
|
-
| Versioned with the code | ✅
|
|
92
|
-
| Portable across agents | ✅ plain Markdown + YAML
|
|
93
|
-
| Typed and queryable | ✅ frontmatter + graph
|
|
94
|
-
| Reviewed in PRs | ✅
|
|
95
|
-
| Scales past one context window | ✅ progressive disclosure
|
|
96
|
-
| Checked by tooling | ✅ (`okf validate` + `lint`)
|
|
97
|
-
|
|
98
|
-
The last
|
|
99
|
-
|
|
91
|
+
| | OKF bundle (this) | `CLAUDE.md` / `AGENTS.md` | Agent auto-memory | Wiki / Notion |
|
|
92
|
+
| ------------------------------ | ----------------------------------------------------- | -------------------------- | ------------------------ | ---------------- |
|
|
93
|
+
| Holds | curated team knowledge | standing instructions | what one agent picked up | human docs |
|
|
94
|
+
| Versioned with the code | ✅ | ✅ | ❌ | ❌ |
|
|
95
|
+
| Portable across agents | ✅ plain Markdown + YAML | ⚠️ per-harness conventions | ❌ per-agent store | ⚠️ export needed |
|
|
96
|
+
| Typed and queryable | ✅ frontmatter + graph | ❌ prose | ❌ | ⚠️ partially |
|
|
97
|
+
| Reviewed in PRs | ✅ | ✅ | ❌ implicit | ⚠️ rarely |
|
|
98
|
+
| Scales past one context window | ✅ progressive disclosure<br>(`okf index` + `search`) | ❌ loaded whole | ⚠️ partially | n/a |
|
|
99
|
+
| Checked by tooling | ✅ exit codes for CI<br>(`okf validate` + `lint`) | ❌ | ❌ | ❌ |
|
|
100
|
+
|
|
101
|
+
The last two rows are this gem's job. Scaling past one context window is
|
|
102
|
+
progressive disclosure — `okf index` reads the map, `okf search` pulls only the
|
|
103
|
+
concepts a task needs, so the bundle is never loaded whole. And drift never
|
|
104
|
+
hides here: the other homes have no detector, but `okf validate` and `lint` turn
|
|
105
|
+
a bundle's drift into findings you can gate on in CI.
|
|
100
106
|
|
|
101
107
|
## What a bundle looks like
|
|
102
108
|
|
|
@@ -158,6 +164,38 @@ putting the `okf` command on your `PATH`:
|
|
|
158
164
|
bundle exec rake install
|
|
159
165
|
```
|
|
160
166
|
|
|
167
|
+
### Run it with Docker (no Ruby needed)
|
|
168
|
+
|
|
169
|
+
Prefer not to install Ruby? The official image bundles the CLI, so every `okf`
|
|
170
|
+
command runs against a bundle you mount at `/data`:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# validate / lint / search / index … mirror the CLI, over the mounted bundle
|
|
174
|
+
docker run --rm -v "$PWD:/data" ghcr.io/serradura/okf validate .
|
|
175
|
+
|
|
176
|
+
# serve the live graph: bind 0.0.0.0 so the host can reach it, and publish the port
|
|
177
|
+
docker run --rm -v "$PWD:/data" -p 8808:8808 ghcr.io/serradura/okf server . --bind 0.0.0.0
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Then open <http://127.0.0.1:8808>. Images are published for `linux/amd64` and
|
|
181
|
+
`linux/arm64` on
|
|
182
|
+
[ghcr.io](https://github.com/serradura/okf-gem/pkgs/container/okf): `:latest`
|
|
183
|
+
tracks the newest release, or pin a version like `:1.5.0`.
|
|
184
|
+
|
|
185
|
+
Tired of the long line? Install a Docker-backed [`okf` command](https://docker.okfgem.com),
|
|
186
|
+
so every verb drops the `docker run` prefix and reads exactly like the native CLI
|
|
187
|
+
(mount, port, and bind handled for you). Do this only on a machine without the
|
|
188
|
+
gem:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
curl -fsSL https://docker.okfgem.com/install.sh | sh # or grab the script by hand
|
|
192
|
+
okf validate .
|
|
193
|
+
okf server .
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
On Windows the image runs under Docker Desktop (WSL2); install with PowerShell
|
|
197
|
+
instead: `irm https://docker.okfgem.com/install.ps1 | iex`.
|
|
198
|
+
|
|
161
199
|
## Command line
|
|
162
200
|
|
|
163
201
|
```bash
|
|
@@ -167,6 +205,7 @@ okf loose <dir> [--json] # list files with no gra
|
|
|
167
205
|
okf search <dir> <term…> [-e] [--in a,b] [...] # ranked text retrieval across metadata + bodies
|
|
168
206
|
okf index <dir> [--json] [--area A] [--no-body] # progressive-disclosure map (§6): bodies, rollups, listings
|
|
169
207
|
okf server <dir> [-p PORT] [--bind ADDR] [...] # serve the interactive graph over HTTP
|
|
208
|
+
okf render <dir> [-o FILE] [--layout NAME] [...] # export the graph as one static, self-contained HTML file
|
|
170
209
|
okf graph <dir> [--json] [--minimal] [--no-body] # print the knowledge graph
|
|
171
210
|
okf catalog | files | tags | types | stats <dir> [--json] # the browser views, on the CLI
|
|
172
211
|
okf skill <dest> [--here] [--force] # install the companion agent skill
|
|
@@ -186,6 +225,8 @@ OKF v0.1 conformance — docs
|
|
|
186
225
|
|
|
187
226
|
$ okf server docs
|
|
188
227
|
serving 37 concepts at http://127.0.0.1:8808 (Ctrl-C to stop)
|
|
228
|
+
|
|
229
|
+
$ okf render docs > public/index.html # the same page, static — host it anywhere
|
|
189
230
|
```
|
|
190
231
|
|
|
191
232
|
<picture>
|
|
@@ -197,6 +238,10 @@ _The graph server on this repo's own [`.okf`](.okf) bundle, with the
|
|
|
197
238
|
`capabilities/graph-server` concept selected. Try it live at
|
|
198
239
|
**[demo.okfgem.com](https://demo.okfgem.com)**._
|
|
199
240
|
|
|
241
|
+
To skip the server entirely, **`okf render <dir>`** writes that same page as one
|
|
242
|
+
self-contained HTML file, the whole bundle baked in, so you can publish the
|
|
243
|
+
graph on GitHub Pages or any static host.
|
|
244
|
+
|
|
200
245
|
`graph` and `server` are best-effort (§9): a file with invalid frontmatter is
|
|
201
246
|
skipped (and noted on stderr), not fatal, so one bad file never breaks the rest.
|
|
202
247
|
The [graph server](.okf/capabilities/graph-server.md) concept walks the request
|
|
@@ -247,15 +292,15 @@ skill plus a post-edit curation hook.
|
|
|
247
292
|
The skill routes a small set of verbs. In Claude Code they run as `/okf:gem
|
|
248
293
|
<verb>`; used standalone, the skill infers the verb from your request.
|
|
249
294
|
|
|
250
|
-
| Verb | What it does
|
|
251
|
-
| ---------------- |
|
|
252
|
-
| _(none)_ | Orient on the bundle and recommend the highest-value next move
|
|
253
|
-
| `search` | Answer a question from the bundle, token-lean: the map, the finder, only the winning bodies
|
|
254
|
-
| `produce` | Create or extend a bundle from code, docs, or knowledge in people's heads
|
|
255
|
-
| `maintain` | Sync the bundle's content with reality after the code or docs change
|
|
256
|
-
| `consume` | Use the bundle as context for a task, writing back what you learn
|
|
257
|
-
| `curate` | Structural upkeep as it stands: `validate` + `lint` + `loose`
|
|
258
|
-
| `doctor` | Install and verify the CLI, then doctor the bundle
|
|
295
|
+
| Verb | What it does |
|
|
296
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| _(none)_ | Orient on the bundle and recommend the highest-value next move |
|
|
298
|
+
| `search` | Answer a question from the bundle, token-lean: the map, the finder, only the winning bodies |
|
|
299
|
+
| `produce` | Create or extend a bundle from code, docs, or knowledge in people's heads |
|
|
300
|
+
| `maintain` | Sync the bundle's content with reality after the code or docs change |
|
|
301
|
+
| `consume` | Use the bundle as context for a task, writing back what you learn |
|
|
302
|
+
| `curate` | Structural upkeep as it stands: `validate` + `lint` + `loose` |
|
|
303
|
+
| `doctor` | Install and verify the CLI, then doctor the bundle |
|
|
259
304
|
| `<okf-cli-verb>` | Run any CLI verb (`validate`, `lint`, `search`, `index`, `server`, the read views) and interpret its output |
|
|
260
305
|
|
|
261
306
|
Point it at your agent's config directory (or its skills directory) and the tree
|
data/lib/okf/cli.rb
CHANGED
|
@@ -55,6 +55,7 @@ module OKF
|
|
|
55
55
|
when "types" then types(argv)
|
|
56
56
|
when "stats" then stats(argv)
|
|
57
57
|
when "server" then server(argv)
|
|
58
|
+
when "render" then render(argv)
|
|
58
59
|
when "skill" then skill(argv)
|
|
59
60
|
when "version", "--version", "-v" then @out.puts(OKF::VERSION); 0
|
|
60
61
|
when "help", "--help", "-h" then usage(@out); 0
|
|
@@ -219,6 +220,34 @@ module OKF
|
|
|
219
220
|
@runner.call(app, options[:bind], options[:port])
|
|
220
221
|
end
|
|
221
222
|
|
|
223
|
+
# The static counterpart to `server`: bake the whole bundle into one
|
|
224
|
+
# self-contained HTML file (bodies, catalog, index, logs baked in, no server
|
|
225
|
+
# needed — e.g. hosting on GitHub Pages). Prints to stdout unless -o is given.
|
|
226
|
+
def render(argv)
|
|
227
|
+
require "okf/server/app"
|
|
228
|
+
|
|
229
|
+
options = { output: nil, title: nil, link: nil, layout: "cose" }
|
|
230
|
+
parser = OptionParser.new do |o|
|
|
231
|
+
o.banner = "Usage: okf render <bundle-dir> [-o FILE] [--layout NAME] [-t title] [-l url]"
|
|
232
|
+
o.on("-o", "--output FILE", "write to FILE instead of stdout") { |v| options[:output] = v }
|
|
233
|
+
o.on("-t", "--title TITLE", "graph title (default: parent/bundle dir name)") { |v| options[:title] = v }
|
|
234
|
+
o.on("-l", "--link URL", "source URL shown in the header") { |v| options[:link] = v }
|
|
235
|
+
o.on("--layout NAME", OKF::Server::Graph::LAYOUTS, "initial layout (#{OKF::Server::Graph::LAYOUTS.join(", ")})") { |v| options[:layout] = v }
|
|
236
|
+
end
|
|
237
|
+
dir = positional_dir(parser, argv) or return 2
|
|
238
|
+
|
|
239
|
+
folder = OKF::Bundle::Folder.load(dir)
|
|
240
|
+
report_skipped(folder)
|
|
241
|
+
html = OKF::Server::App.new(folder, title: options[:title] || folder.name, link: options[:link], layout: options[:layout]).render_static
|
|
242
|
+
if options[:output]
|
|
243
|
+
File.write(options[:output], html)
|
|
244
|
+
@out.puts "wrote #{folder.graph(minimal: true).nodes.size} concepts to #{options[:output]}"
|
|
245
|
+
else
|
|
246
|
+
@out.print html
|
|
247
|
+
end
|
|
248
|
+
0
|
|
249
|
+
end
|
|
250
|
+
|
|
222
251
|
def graph(argv)
|
|
223
252
|
options = { json: false, minimal: false, body: true }
|
|
224
253
|
parser = OptionParser.new do |o|
|
|
@@ -947,6 +976,7 @@ module OKF
|
|
|
947
976
|
|
|
948
977
|
skill <dest> [--here] [--force] install the companion agent skill
|
|
949
978
|
server <dir> [-p PORT] [--bind ADDR] [...] serve an interactive HTML graph
|
|
979
|
+
render <dir> [-o FILE] [--layout NAME] [...] write a static, self-contained HTML graph
|
|
950
980
|
|
|
951
981
|
lint <dir> [--json] [--fail-on warn] [...] report curation-quality issues
|
|
952
982
|
loose <dir> [--json] list files with no graph links, by folder
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -55,6 +55,13 @@ module OKF
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# The same interactive page, but with the whole bundle baked in — bodies,
|
|
59
|
+
# catalog, index and logs — so it needs no server. This is what `okf render`
|
|
60
|
+
# writes: the fetch getters resolve from the embedded payload, not from here.
|
|
61
|
+
def render_static
|
|
62
|
+
Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout, embed: embed_payload).render
|
|
63
|
+
end
|
|
64
|
+
|
|
58
65
|
private
|
|
59
66
|
|
|
60
67
|
# The minimal graph snapshot taken at boot — drives the page and the indexes.
|
|
@@ -96,6 +103,29 @@ module OKF
|
|
|
96
103
|
@page ||= Graph.new(graph, title: @title || @folder.name, link: @link, layout: @layout).render
|
|
97
104
|
end
|
|
98
105
|
|
|
106
|
+
# Everything the on-demand endpoints would serve, baked for render mode. The
|
|
107
|
+
# arrays match what each client getter extracts from the JSON envelope; the
|
|
108
|
+
# per-concept maps mirror /node (raw, unstripped body) and /node/meta (the
|
|
109
|
+
# same escaped fragment). Read from the in-memory bundle — no live disk read,
|
|
110
|
+
# since a static file is a snapshot, not a window on edits.
|
|
111
|
+
def embed_payload
|
|
112
|
+
{
|
|
113
|
+
catalog: @folder.catalog,
|
|
114
|
+
index: @folder.directory_index,
|
|
115
|
+
logs: logs[:logs],
|
|
116
|
+
bodies: bodies,
|
|
117
|
+
meta: meta
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def bodies
|
|
122
|
+
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def meta
|
|
126
|
+
@folder.bundle.concepts.each_with_object({}) { |concept, map| map[concept.id] = description_fragment(concept) }
|
|
127
|
+
end
|
|
128
|
+
|
|
99
129
|
def node_body(id)
|
|
100
130
|
concept = concept_for(id)
|
|
101
131
|
return not_found if concept.nil?
|
|
@@ -496,6 +496,11 @@
|
|
|
496
496
|
const NODES=<%= nodes_json %>, EDGES=<%= edges_json %>, TYPES=<%= types_json %>, TAGS=<%= tags_json %>;
|
|
497
497
|
const NODE_ENDPOINT=<%= @node_endpoint.to_json %>, META_ENDPOINT=<%= @meta_endpoint.to_json %>;
|
|
498
498
|
const CATALOG_ENDPOINT="catalog",INDEX_ENDPOINT="index",LOG_ENDPOINT="log";
|
|
499
|
+
/* null when served live (`okf server`) — the getters below fetch the endpoints
|
|
500
|
+
above. `okf render` injects a payload {catalog,index,logs,bodies,meta}, and
|
|
501
|
+
every getter resolves from it instead, so one file needs no server and the
|
|
502
|
+
endpoint consts above go inert. */
|
|
503
|
+
const EMBED=<%= embed_json %>;
|
|
499
504
|
const MIN=<%= OKF::Server::Graph::MIN_SIZE %>, MAX=<%= OKF::Server::Graph::MAX_SIZE %>;
|
|
500
505
|
const PALETTE=["#6E56CF","#D97757","#22C55E","#3B82F6","#EAB308","#EC4899","#14B8A6","#F97316","#A855F7","#0EA5E9","#84CC16","#EF4444","#64748B"];
|
|
501
506
|
const root=document.documentElement;
|
|
@@ -701,6 +706,12 @@ function showLog(path){LOGS=null;getLogs().then(logs=>{const l=logs.find(x=>x.pa
|
|
|
701
706
|
sideBody.innerHTML=`<span class="type">update log · §7</span><h2 class="title">${esc(path)}</h2><div class="body" id="dir-body"></div>`;
|
|
702
707
|
renderMarkdown(document.getElementById('dir-body'),l.content||'');
|
|
703
708
|
if(opened)requestAnimationFrame(()=>cy.resize());});}
|
|
709
|
+
/* Two per-concept reads shared by the inspector and the files preview: the raw
|
|
710
|
+
markdown body (rendered client-side) and the description fragment. Live they
|
|
711
|
+
hit /node and /node/meta; under EMBED they resolve from the baked payload.
|
|
712
|
+
Never memoized in server mode, so on-disk edits keep reflecting. */
|
|
713
|
+
function getNodeBody(id){return EMBED?Promise.resolve(EMBED.bodies[id]||''):fetch(NODE_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.text():'');}
|
|
714
|
+
function getNodeMeta(id){return EMBED?Promise.resolve(EMBED.meta[id]||''):fetch(META_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.text():'');}
|
|
704
715
|
function show(id){const n=byId[id];if(!n)return false;const ty=typeOf[id]||'Untyped';const c=color[ty]||'#64748b';
|
|
705
716
|
shownId=id;const opened=openPanel();sideBody.scrollTop=0;
|
|
706
717
|
sideBody.innerHTML=`<span class="type"><span class="dot" style="background:${c}"></span>${esc(ty)}</span>
|
|
@@ -708,10 +719,9 @@ function show(id){const n=byId[id];if(!n)return false;const ty=typeOf[id]||'Unty
|
|
|
708
719
|
<div class="tags">${(tagsOf[id]||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div>
|
|
709
720
|
${relList('Links to',outL[id])}${relList('Linked from',inL[id])}<div class="body" id="body"></div>`;
|
|
710
721
|
sideBody.querySelectorAll('[data-go]').forEach(a=>a.onclick=()=>select(a.getAttribute('data-go')));
|
|
711
|
-
const
|
|
712
|
-
fetch(META_ENDPOINT+'?id='+eid).then(r=>r.ok?r.text():'').then(h=>{const d=document.getElementById('desc');if(d){d.innerHTML=h||'<span class="empty">no description</span>';d.classList.remove('loading');}}).catch(()=>{});
|
|
722
|
+
getNodeMeta(id).then(h=>{const d=document.getElementById('desc');if(d){d.innerHTML=h||'<span class="empty">no description</span>';d.classList.remove('loading');}}).catch(()=>{});
|
|
713
723
|
const bodyEl=sideBody.querySelector('#body');
|
|
714
|
-
|
|
724
|
+
getNodeBody(id).then(md=>{if(bodyEl.isConnected)renderMarkdown(bodyEl,md);}).catch(()=>{});
|
|
715
725
|
return opened;}
|
|
716
726
|
/* One camera move per selection. When the click also opens the panel, the
|
|
717
727
|
container is still animating (--side-w, .22s), so pan now and the debounced
|
|
@@ -889,7 +899,7 @@ searchInput.oninput=()=>{q[view]=searchInput.value;
|
|
|
889
899
|
|
|
890
900
|
/* ── catalog ── */
|
|
891
901
|
let CATALOG=null;
|
|
892
|
-
function getCatalog(){return CATALOG||(CATALOG=fetch(CATALOG_ENDPOINT).then(r=>r.json()).then(d=>d.concepts));}
|
|
902
|
+
function getCatalog(){return CATALOG||(CATALOG=EMBED?Promise.resolve(EMBED.catalog):fetch(CATALOG_ENDPOINT).then(r=>r.json()).then(d=>d.concepts));}
|
|
893
903
|
const catActiveTypes=new Set(), catActiveAreas=new Set(), catActiveTags=new Set();
|
|
894
904
|
const catByCount=types.slice().sort((a,b)=>(TYPES[b]||[]).length-(TYPES[a]||[]).length);
|
|
895
905
|
const catChip=t=>`<span class="chip" data-t="${esc(t)}"><span class="dot" style="background:${color[t]}"></span>${esc(t)} <span class="c">${(TYPES[t]||[]).length}</span></span>`;
|
|
@@ -1024,7 +1034,7 @@ function openFile(id,list){fileSel=id;const c=(list||[]).find(x=>x.id===id)||byI
|
|
|
1024
1034
|
document.getElementById('fp-graph').hidden=false;
|
|
1025
1035
|
document.getElementById('fp-graph').onclick=()=>goToGraph(id);
|
|
1026
1036
|
const body=document.getElementById('fp-body');body.innerHTML='<p class="loading">loading…</p>';
|
|
1027
|
-
|
|
1037
|
+
getNodeBody(id).then(txt=>{
|
|
1028
1038
|
const wrap=document.createElement('div');wrap.className='body';renderMarkdown(wrap,txt);body.innerHTML='';body.appendChild(wrap);}).catch(()=>{body.innerHTML='<p class="empty">could not load this file</p>';});}
|
|
1029
1039
|
/* Reserved files read in the preview too: the index map from its payload, the
|
|
1030
1040
|
log fetched fresh on every click so a just-appended entry shows. */
|
|
@@ -1119,13 +1129,13 @@ document.addEventListener('fullscreenchange',()=>btnFull.setAttribute('aria-pres
|
|
|
1119
1129
|
|
|
1120
1130
|
/* ── the §6 map payload — feeds folder clicks, the Indexes tab, and previews ── */
|
|
1121
1131
|
let INDEX=null;
|
|
1122
|
-
function getIndex(){return INDEX||(INDEX=fetch(INDEX_ENDPOINT).then(r=>r.json()).then(d=>d.directories));}
|
|
1132
|
+
function getIndex(){return INDEX||(INDEX=EMBED?Promise.resolve(EMBED.index):fetch(INDEX_ENDPOINT).then(r=>r.json()).then(d=>d.directories));}
|
|
1123
1133
|
function listingHtml(items){if(!items||!items.length)return '<p class="empty" style="margin:10px 0 0">No concepts directly here.</p>';
|
|
1124
1134
|
return '<ul class="ix-listing">'+items.map(it=>`<li><a data-go="${esc(it.id)}">${esc(it.title||it.id)}</a>${it.description?` <span class="d">— ${esc(it.description)}</span>`:''}</li>`).join('')+'</ul>';}
|
|
1125
1135
|
|
|
1126
1136
|
/* ── log payload (the §7 history) — read by the Files view's log entries ── */
|
|
1127
1137
|
let LOGS=null;
|
|
1128
|
-
function getLogs(){return LOGS||(LOGS=fetch(LOG_ENDPOINT).then(r=>r.json()).then(d=>d.logs));}
|
|
1138
|
+
function getLogs(){return LOGS||(LOGS=EMBED?Promise.resolve(EMBED.logs):fetch(LOG_ENDPOINT).then(r=>r.json()).then(d=>d.logs));}
|
|
1129
1139
|
|
|
1130
1140
|
/* ── keyboard ── */
|
|
1131
1141
|
const VIEW_KEYS={'1':'graph','3':'files','4':'catalog','5':'tags','6':'stats'};
|
data/lib/okf/server/graph.rb
CHANGED
|
@@ -7,15 +7,23 @@ module OKF
|
|
|
7
7
|
# returns the HTML string.
|
|
8
8
|
#
|
|
9
9
|
# The page boots from a *minimal* payload — nodes carry only id + title, plus
|
|
10
|
-
# compact TYPES/TAGS inverted indexes for colouring and filtering
|
|
11
|
-
#
|
|
12
|
-
# fetch, rendering it client-side with marked. Node bodies are therefore NOT
|
|
13
|
-
# embedded here.
|
|
10
|
+
# compact TYPES/TAGS inverted indexes for colouring and filtering. It has two
|
|
11
|
+
# data modes, both driven by one template:
|
|
14
12
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# (
|
|
13
|
+
# server mode (embed: nil) — the default. Each concept's markdown body,
|
|
14
|
+
# metadata, catalog, index and log are pulled from OKF::Server::App on
|
|
15
|
+
# demand via fetch, so the initial payload stays small and bodies read
|
|
16
|
+
# live from disk (edits show without a restart). Nothing extra embedded.
|
|
17
|
+
# render mode (embed: payload) — `okf render` bakes the whole bundle in:
|
|
18
|
+
# the same fetch getters resolve from the injected payload instead, so
|
|
19
|
+
# the single file needs no server (e.g. hosting on GitHub Pages).
|
|
20
|
+
#
|
|
21
|
+
# NOTE (trust boundary): the page loads Cytoscape + marked from a CDN, so it
|
|
22
|
+
# needs network for those libraries even in render mode. Fetched/embedded
|
|
23
|
+
# markdown is sanitized client-side (DOMPurify.sanitize(marked.parse(...)))
|
|
24
|
+
# and all inline-<script> data — including any embedded body — is
|
|
25
|
+
# </script>-escaped by #json_for_script (stdlib ERB does not auto-escape).
|
|
26
|
+
# Still, only serve bundles you trust.
|
|
19
27
|
class Graph
|
|
20
28
|
TEMPLATE = File.expand_path("graph/template.html.erb", __dir__)
|
|
21
29
|
LAYOUTS = %w[cose concentric breadthfirst circle grid].freeze
|
|
@@ -31,13 +39,15 @@ module OKF
|
|
|
31
39
|
# +node_endpoint+/+meta_endpoint+ are the (mount-relative) URLs the page
|
|
32
40
|
# fetches a concept's raw markdown and metadata fragment from — relative so
|
|
33
41
|
# the page works whether served at "/" or mounted under a Rails prefix.
|
|
34
|
-
|
|
42
|
+
# +embed+ is the render-mode payload (nil = server mode); see the class doc.
|
|
43
|
+
def initialize(graph, title: nil, link: nil, layout: "cose", node_endpoint: "node", meta_endpoint: "node/meta", embed: nil)
|
|
35
44
|
@graph = graph
|
|
36
45
|
@title = title
|
|
37
46
|
@link = link
|
|
38
47
|
@layout = layout
|
|
39
48
|
@node_endpoint = node_endpoint
|
|
40
49
|
@meta_endpoint = meta_endpoint
|
|
50
|
+
@embed = embed
|
|
41
51
|
end
|
|
42
52
|
|
|
43
53
|
def render
|
|
@@ -86,6 +96,12 @@ module OKF
|
|
|
86
96
|
json_for_script(@graph.tag_index)
|
|
87
97
|
end
|
|
88
98
|
|
|
99
|
+
# The render-mode payload, or the literal `null` in server mode — both from
|
|
100
|
+
# the same </script>-escaping helper, so injection stays uniform and safe.
|
|
101
|
+
def embed_json
|
|
102
|
+
json_for_script(@embed)
|
|
103
|
+
end
|
|
104
|
+
|
|
89
105
|
# JSON-encode for safe embedding in an inline <script>: escaping every `<` to
|
|
90
106
|
# its JSON unicode escape neutralizes </script>, <!-- and <script in one
|
|
91
107
|
# stroke, and the result stays valid JSON *and* JavaScript.
|
data/lib/okf/skill/SKILL.md
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: okf
|
|
3
3
|
description: >-
|
|
4
|
-
Be the expert on Open Knowledge Format (OKF) — portable knowledge as a
|
|
5
|
-
of markdown files with YAML frontmatter that
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
carrying `okf_version`. Triggers on: "document this in OKF", "update the
|
|
15
|
-
knowledge bundle", "capture this as a concept", "what do we know about X?",
|
|
16
|
-
"where is X documented?", "search the bundle", "validate/lint/serve the
|
|
17
|
-
bundle", or a task needing knowledge from an OKF bundle already in the repo.
|
|
4
|
+
Be the expert on Open Knowledge Format (OKF) — portable project knowledge as a
|
|
5
|
+
directory of markdown files with YAML frontmatter that humans and agents read
|
|
6
|
+
from one source. Use when capturing knowledge into a bundle (a service, schema,
|
|
7
|
+
metric, decision, runbook: "document this in OKF", "capture this as a concept"),
|
|
8
|
+
retrieving from one without reading it whole ("what do we know about X?", "where
|
|
9
|
+
is X documented?", "search the bundle"), updating one after code or docs change
|
|
10
|
+
("update the knowledge bundle"), checking its conformance or curation quality
|
|
11
|
+
("validate/lint the bundle"), serving or rendering it as a graph, or working in a
|
|
12
|
+
repo that already carries an OKF bundle — a `.okf/` directory or a root `index.md`
|
|
13
|
+
carrying `okf_version`.
|
|
18
14
|
user-invocable: true
|
|
19
15
|
argument-hint: "[search|produce|maintain|consume|<okf-cli-verb>] [dir] [--flags]"
|
|
20
16
|
allowed-tools: Read Write Edit Grep Glob Bash
|
|
@@ -147,7 +143,7 @@ Read the referenced playbook before executing — it *is* the procedure.
|
|
|
147
143
|
| `consume` | Use | use the bundle as context for a task | [playbooks/consume.md](playbooks/consume.md) |
|
|
148
144
|
| `curate` | Curate | structural upkeep as it stands: validate + lint + loose | [playbooks/curate.md](playbooks/curate.md) |
|
|
149
145
|
| `doctor` | Setup | install and verify the CLI, then doctor the bundle | [playbooks/doctor.md](playbooks/doctor.md) |
|
|
150
|
-
| `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, skill | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
|
|
146
|
+
| `<okf-cli-verb>` | Read | validate, lint, loose, index, catalog, files, tags, types, stats, graph, server, render, skill | `okf <verb> --help` + [reference/cli.md](reference/cli.md) |
|
|
151
147
|
|
|
152
148
|
Two boundaries worth keeping sharp: `curate` is structural upkeep only — when
|
|
153
149
|
the *content* no longer matches reality, that is `maintain` — and `doctor` is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# OKF tool verbs — the `okf` CLI
|
|
2
2
|
|
|
3
3
|
`validate`, `lint`, `loose`, `search`, `index`, `catalog`, `files`, `tags`, `types`,
|
|
4
|
-
`stats`, `server`, and `graph` are **not** eyeball passes and are not
|
|
4
|
+
`stats`, `server`, `render`, and `graph` are **not** eyeball passes and are not
|
|
5
5
|
reimplemented in this skill. They run the deterministic `okf` executable shipped by
|
|
6
6
|
the companion gem — the single source of truth for OKF mechanics. Your job is to
|
|
7
7
|
invoke it correctly and interpret the result, not to reason out conformance by hand.
|
|
@@ -39,7 +39,7 @@ difference between a few hundred bytes and hundreds of KB, since the per-item ro
|
|
|
39
39
|
`body`.
|
|
40
40
|
|
|
41
41
|
**Exit codes:** `0` success · `1` non-conformant bundle (or a `lint --fail-on`
|
|
42
|
-
threshold crossed) · `2` usage error. `graph` and `
|
|
42
|
+
threshold crossed) · `2` usage error. `graph`, `server`, and `render` are best-effort
|
|
43
43
|
(§9): a file with invalid frontmatter is skipped and noted on stderr, never fatal.
|
|
44
44
|
|
|
45
45
|
## validate — the hard gate (§9)
|
|
@@ -223,6 +223,23 @@ viewer libraries (Cytoscape, marked, DOMPurify — plus Mermaid and Panzoom,
|
|
|
223
223
|
lazy-loaded on first use) from a CDN and renders whatever
|
|
224
224
|
links the bundle carries — so only serve bundles you trust.
|
|
225
225
|
|
|
226
|
+
## render — static graph export
|
|
227
|
+
|
|
228
|
+
Writes the same interactive page as one static, self-contained HTML file
|
|
229
|
+
(`okf render <dir>`), so the graph hosts where there is no server — GitHub Pages,
|
|
230
|
+
an object store, an attachment. Prints to stdout (`okf render <dir> > graph.html`)
|
|
231
|
+
or writes `-o FILE`; `--title`/`--link`/`--layout` mirror `server`. It is the same
|
|
232
|
+
template `server` renders, one switch apart: rather than fetching each body,
|
|
233
|
+
description, catalog, index, and log live, `render` bakes the whole bundle into
|
|
234
|
+
the page and the browser reads from that embedded payload — no server, no build
|
|
235
|
+
step. The trade-off is weight (every body is inlined), so `server` stays the
|
|
236
|
+
choice for a bundle too large to ship whole.
|
|
237
|
+
|
|
238
|
+
**Trust boundary:** the same two guards as `server` — every inlined body is
|
|
239
|
+
`</script>`-escaped like the graph data and still sanitized by DOMPurify when
|
|
240
|
+
rendered — so a static file is no laxer than the live server. Only render bundles
|
|
241
|
+
you trust.
|
|
242
|
+
|
|
226
243
|
## graph — the raw structure
|
|
227
244
|
|
|
228
245
|
Prints the node/edge graph. `--json` emits a machine-readable dump (`nodes` with
|
data/lib/okf/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: okf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Serradura
|
|
@@ -123,6 +123,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
123
123
|
requirements: []
|
|
124
124
|
rubygems_version: 4.0.16
|
|
125
125
|
specification_version: 4
|
|
126
|
-
summary: '
|
|
127
|
-
|
|
126
|
+
summary: 'The complete toolkit for the Open Knowledge Format: an agent skill, a CLI
|
|
127
|
+
and library, and a live knowledge graph. 100% local.'
|
|
128
128
|
test_files: []
|