diff --git a/.gitea/workflows/deploy-web.yml b/.gitea/workflows/deploy-web.yml
index 4bbf992..cb30848 100644
--- a/.gitea/workflows/deploy-web.yml
+++ b/.gitea/workflows/deploy-web.yml
@@ -78,7 +78,9 @@ jobs:
- name: Build dist/web
run: |
- python3 tools/build_web.py
+ # the agent window's default endpoint: the site's relay to B4L's
+ # inference server (deploy/Caddyfile, handle_path /llm/*)
+ python3 tools/build_web.py --agent-url /llm/v1
echo "$GITHUB_SHA" > dist/web/version.txt
- name: Set up SSH
diff --git a/Cargo.lock b/Cargo.lock
index f7eecfe..1d4a9f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
[[package]]
name = "ahash"
version = "0.8.12"
@@ -52,7 +58,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -63,7 +69,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -84,6 +90,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "bitflags"
version = "2.13.2"
@@ -96,6 +108,12 @@ version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
[[package]]
name = "castaway"
version = "0.2.4"
@@ -105,6 +123,16 @@ dependencies = [
"rustversion",
]
+[[package]]
+name = "cc"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.5"
@@ -197,6 +225,15 @@ dependencies = [
"tiny-keccak",
]
+[[package]]
+name = "crc32fast"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "crunchy"
version = "0.2.4"
@@ -209,6 +246,23 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
[[package]]
name = "futures-core"
version = "0.3.34"
@@ -270,6 +324,22 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
[[package]]
name = "httpdate"
version = "1.0.3"
@@ -301,6 +371,7 @@ dependencies = [
"isokit-project",
"serde_json",
"tiny_http",
+ "ureq",
]
[[package]]
@@ -385,6 +456,16 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -409,6 +490,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -476,6 +563,55 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -534,6 +670,18 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
[[package]]
name = "slab"
version = "0.4.12"
@@ -572,6 +720,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "2.0.119"
@@ -666,6 +820,47 @@ version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b"
+dependencies = [
+ "base64",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -748,12 +943,30 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webpki-roots"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -763,6 +976,70 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
[[package]]
name = "winnow"
version = "1.0.4"
@@ -795,6 +1072,18 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/Cargo.toml b/Cargo.toml
index dc27666..c1ca5a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,8 @@ serde_json = "1"
toml = { version = "1", features = ["preserve_order"] }
rhai = { version = "1.27", default-features = true }
tiny_http = "0.12"
+# the Studio's agent window reaches the language model through `isokit studio`
+ureq = "3"
clap = { version = "4", features = ["derive"] }
anyhow = "1"
web-time = "1"
diff --git a/README.md b/README.md
index a49fd8a..9322ecd 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,8 @@ click for details. One Rust binary does everything:
- **Web Studio**: the same Studio running entirely in the browser, with the
engine compiled to WebAssembly. Nothing to install: projects live in the
browser, or in a folder on disk (Chrome / Edge) that agents edit alongside.
+- **Agent window**: chat with a language model right in the Studio (by default
+ B4L's inference server); it edits the scene with the MCP tools while you watch.
- **MCP server**: AI agents (Hermes and others) edit the same scene with the
same operations; their changes show up live in an open Studio.
- **Build**: a self-contained HTML page with every view prerendered, or
@@ -80,6 +82,33 @@ MCP tools also run inside the page: `await isokit.mcp({jsonrpc: "2.0", id: 1,
method: "tools/list"})`, and browsers with WebMCP (`navigator.modelContext`)
get them as page tools.
+### Agent window
+
+**✦ Agent** in the Studio's top bar opens a chat with a language model that
+edits the scene for you: "plant a row of trees along the road", "make the
+workshop one storey taller", "what is on this site?". It works with the 15 MCP
+tools below plus two page tools (point at an object, turn the view), streams
+its answer and its reasoning, and shows each tool call (click one for the
+arguments and the result, click an object id to select it). Every edit lands
+on the map at once, and Undo takes it back. Stop ends a turn; the window moves
+by its title bar and resizes at its top-left corner.
+
+Any OpenAI-compatible endpoint with tool calling works (vLLM, llama.cpp,
+Ollama, LM Studio, hosted APIs). The default is B4L's inference server,
+`https://inference.p0.contact/v1`; ⚙ sets endpoint, key, model and whether
+the model thinks first.
+
+- `isokit studio` passes the calls through its own server (`/api/agent/*`), so
+ the key never reaches the page and the endpoint needs no CORS. Settings:
+ `ISOKIT_LLM_URL`, `ISOKIT_LLM_KEY`, `ISOKIT_LLM_MODEL`, or the window's ⚙,
+ which saves them in `%APPDATA%\isokit\agent.json` (`~/.config/isokit` elsewhere).
+- The web Studio calls the endpoint from the page; the key stays in that
+ browser. The endpoint must allow the page (CORS, preflight included), or the
+ site relays it: https://isokit.buildfor.life/ is built with
+ `--agent-url /llm/v1`, a same-site path that Caddy forwards to B4L's
+ inference server (`deploy/Caddyfile`). The relay adds no key: each browser
+ enters its own once.
+
### Agents (MCP)
While the Studio runs, it also serves MCP over HTTP at `http://127.0.0.1:7878/mcp`.
@@ -127,7 +156,7 @@ storey.
| Command | |
|---|---|
| `isokit new
` | starter project |
-| `isokit studio [dir] [--port N] [--no-open]` | GUI editor + `/mcp` + `/view` live preview |
+| `isokit studio [dir] [--port N] [--no-open]` | GUI editor + `/mcp` + `/view` live preview + the agent window's link to the model |
| `isokit mcp [dir]` | MCP over stdio |
| `isokit build [dir] [-o file]` | self-contained HTML (all levels × 4 rotations) |
| `isokit render [dir] -o x.svg [--level gf] [--rot 2] [--light]` | one view as SVG |
@@ -144,7 +173,7 @@ crates/isokit-project project folder, document ops, export, check, Workspace (u
crates/isokit-cli the `isokit` binary: Studio HTTP server, MCP stdio, build/render/check
crates/isokit-wasm the engine for the browser (wasm-bindgen): a project in memory behind the Studio API
viewer/ isokit-view.js/.css: pan, zoom, levels, rotation, tooltips (any page can embed it)
-studio/ the Studio GUI (served by `isokit studio`, and by the web Studio)
+studio/ the Studio GUI (served by `isokit studio`, and by the web Studio); agent.js = agent window
web/ web Studio shell: WASM backend for studio.js, projects, folders, zip, code editor
examples/demo demo project
deploy/Caddyfile the web Studio's vhost on the buildfor.life web server (s1)
@@ -165,12 +194,16 @@ Plans: [docs/ROADMAP.md](docs/ROADMAP.md).
```bash
cargo test # engine, runtime, ops, demo, MCP
cargo clippy --all-targets -- -D warnings
-isokit studio examples/demo --port 7890 --no-open &
-uv run --with playwright python tools/check_studio.py http://127.0.0.1:7890 # GUI + MCP end to end (Edge)
+ISOKIT_LLM_URL=http://127.0.0.1:7932/v1 ISOKIT_LLM_KEY=test-key ISOKIT_CONFIG_DIR= \
+ isokit studio --port 7890 --no-open & # agent window on tools/mock_llm.py
+uv run --with playwright python tools/check_studio.py http://127.0.0.1:7890 # GUI + MCP + agent window end to end (Edge)
cargo clippy -p isokit-wasm --target wasm32-unknown-unknown -- -D warnings
python tools/build_web.py && uv run --with playwright python tools/check_web.py # web Studio end to end
```
+The agent window checks run on `tools/mock_llm.py`, a scripted stand-in for an
+OpenAI-compatible server that the check scripts start on port 7932.
+
`python tools/build_web.py --skip-cargo` re-copies the front end (studio/,
viewer/, web/) without rebuilding the WASM. Code that runs in the browser must
not use `std::time` (it traps on wasm32-unknown-unknown): use `web_time`.
diff --git a/crates/isokit-cli/Cargo.toml b/crates/isokit-cli/Cargo.toml
index dd8b895..1116e61 100644
--- a/crates/isokit-cli/Cargo.toml
+++ b/crates/isokit-cli/Cargo.toml
@@ -13,5 +13,6 @@ path = "src/main.rs"
isokit-project = { workspace = true }
serde_json = { workspace = true }
tiny_http = { workspace = true }
+ureq = { workspace = true }
clap = { workspace = true }
anyhow = { workspace = true }
diff --git a/crates/isokit-cli/src/agent.rs b/crates/isokit-cli/src/agent.rs
new file mode 100644
index 0000000..175e396
--- /dev/null
+++ b/crates/isokit-cli/src/agent.rs
@@ -0,0 +1,261 @@
+//! The Studio's agent window talks to a language model through these routes,
+//! so the endpoint key stays on this machine and the browser needs no CORS
+//! permission from the endpoint (any OpenAI-compatible `/v1` API: vLLM,
+//! llama.cpp, Ollama, LM Studio, a hosted one):
+//!
+//! - `GET /api/agent/config` endpoint, model, whether a key is set (never the key)
+//! - `POST /api/agent/config` save `{ url, model, think, key }` (key "" forgets it)
+//! - `GET /api/agent/models` the endpoint's `/models`
+//! - `POST /api/agent/chat` the endpoint's `/chat/completions`, streamed through
+//!
+//! Settings come from `ISOKIT_LLM_URL`, `ISOKIT_LLM_KEY` and `ISOKIT_LLM_MODEL`,
+//! else from `agent.json` in the user's config folder (`%APPDATA%\isokit`,
+//! `~/.config/isokit`; `ISOKIT_CONFIG_DIR` moves it), which the window writes.
+
+use serde_json::{json, Map, Value};
+use std::io::{Read, Write};
+use std::path::PathBuf;
+use std::time::Duration;
+use tiny_http::Request;
+
+/// B4L's inference server (vLLM, Qwen); any OpenAI-compatible base URL works.
+pub const DEFAULT_URL: &str = "https://inference.p0.contact/v1";
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Settings {
+ pub url: String,
+ pub key: String,
+ pub model: String,
+ pub think: bool,
+ /// where url / key came from: "env", "file" or "default" / ""
+ pub url_from: &'static str,
+ pub key_from: &'static str,
+}
+
+pub fn config_file() -> Option {
+ if let Some(d) = var("ISOKIT_CONFIG_DIR") {
+ return Some(PathBuf::from(d).join("agent.json"));
+ }
+ let base = if cfg!(windows) {
+ var("APPDATA").map(PathBuf::from)
+ } else {
+ var("XDG_CONFIG_HOME")
+ .map(PathBuf::from)
+ .or_else(|| var("HOME").map(|h| PathBuf::from(h).join(".config")))
+ };
+ base.map(|b| b.join("isokit").join("agent.json"))
+}
+
+fn var(k: &str) -> Option {
+ std::env::var(k)
+ .ok()
+ .map(|v| v.trim().to_string())
+ .filter(|v| !v.is_empty())
+}
+
+fn read_file() -> Map {
+ config_file()
+ .and_then(|p| std::fs::read_to_string(p).ok())
+ .and_then(|s| serde_json::from_str::