---
name: heivol-contextbox
description: |
  Search, read, and manage the user's heivol-contextbox knowledge bases.
  Use when the user references notes, recipes, family documents, travel plans, or any
  private knowledge stored in contextbox — and when the user wants to create/delete
  KBs, upload/delete docs, share KBs (invites, share links), manage members, accept
  invites, or mint/revoke tokens. Triggers on phrases like "find in my notes", "what
  do I have on X", "search contextbox", "check my KB for Y", "upload this to my
  contextbox", "create a KB", "delete doc from my KB", "share my KB with...", "invite
  to my KB", or any question answerable by the user's private KBs before falling back
  to general knowledge. Also use after the user uploads new documents to verify
  retrievability.
---

# heivol-contextbox

Multi-tenant KB search + management. Endpoint `https://contextbox.heivol.com` (CT 205, CF tunnel). Roles per KB: `reader` < `editor` < `owner`. Token scopes: `read` vs `write`. Full guide at `/guide`.

Use BEFORE answering from general knowledge whenever the question could plausibly be in the user's notes. Never fabricate hits — say "no results" plainly. **Never read or echo token values** — call the `bin/cb-*` scripts; they resolve tokens internally so the secret never enters the transcript.

## Scripts (primary surface)

The bundle ships `bin/cb-*` helpers that handle token resolution and HTTP. **Use these first** — they keep tokens off your context window. Raw `curl` is a fallback only if a script is missing or doesn't fit.

```bash
SBD="$HOME/.claude/skills/heivol-contextbox"   # or wherever the bundle was unzipped

$SBD/bin/cb-search "<query>" [--kb=<slug>] [--limit=N]    # hybrid search
$SBD/bin/cb-list-kbs                                       # list reachable KBs (incl. permissions)
$SBD/bin/cb-list-docs <slug> [--prefix=p] [--limit=N]      # enumerate
$SBD/bin/cb-read-doc <slug> <path>                         # read one
$SBD/bin/cb-upload-doc <slug> <path> [<file>]              # PUT (stdin if no file)
$SBD/bin/cb-me [<slug>]                                    # /api/me — confirm scope
$SBD/bin/cb-list-vaults                                    # locally configured (label, kb_slug) pairs
$SBD/bin/cb-resolve-vault "<label-substring>"              # → kb_slug (case-insensitive)
$SBD/bin/cb-rename-token <token-id> "<new label>"          # PATCH label
echo "<TOKEN>" | $SBD/bin/cb-add-vault <slug> "<label>"    # wire up a new vault
```

`cb-add-vault` is the **only** path that takes a token literal — and only via stdin, never argv. After install, all other scripts read tokens from disk.

## Auth

The install bundle ships **one or more token files** under `<skill base dir>/tokens/`. Each token can be either user-scoped (works on every KB you're a member of) or KB-scoped (works only on one KB — strict blast-radius for shared KBs you don't own). Each token has a sibling `<scope>.meta.json` carrying its label and KB binding.

```
<skill base dir>/
├── SKILL.md
├── tokens/
│   ├── default                  ← user-scoped token
│   ├── default.meta.json        ← { "label": "laptop", "kb_slug": null }
│   ├── UhLyf_Tw7FOCqxhclYZlr    ← KB-scoped token
│   └── UhLyf_Tw7FOCqxhclYZlr.meta.json  ← { "label": "work-notes", "kb_slug": "UhLyf_Tw7FOCqxhclYZlr" }
└── token  →  tokens/default     (legacy symlink)
```

KB slugs are 21-char nanoids (e.g. `UhLyf_Tw7FOCqxhclYZlr`) — opaque to humans. Use the **label** in `<scope>.meta.json` whenever you talk to the user about a vault.

**Token resolution — pick the most specific match for the KB the call references:**

1. `$HEIVOL_CONTEXTBOX_TOKEN` env var (overrides everything)
2. `<skill base dir>/tokens/<call_kb_slug>` — when the call names a KB and a matching token exists, use it
3. `<skill base dir>/tokens/default` — fallback for cross-KB or unscoped calls
4. `<skill base dir>/token` — legacy single-file path; on new installs this is a symlink to `tokens/default`
5. `~/.claude/skills/heivol-contextbox/tokens/<scope>` (Claude Code user-scope)
6. `~/.claude/skills/heivol-contextbox/token` (legacy)
7. `~/.config/contextbox/token` (legacy)

### Resolve a vault by label

When the user references a vault by its human label ("search my work vault", "list docs in family"), resolve label → slug from the meta files:

```bash
SBD="$HOME/.claude/skills/heivol-contextbox"
LABEL_QUERY="work"   # case-insensitive substring
SLUG=$(for f in "$SBD"/tokens/*.meta.json; do
  jq -r --arg q "$LABEL_QUERY" '
    select(.label != null and (.label | ascii_downcase | contains($q | ascii_downcase)))
    | .kb_slug // "default"
  ' "$f" 2>/dev/null
done | head -1)
# Use $SLUG with --data-urlencode kb_slug=... and "$SBD/tokens/$SLUG" as the bearer source.
```

If nothing matches, ask the user to disambiguate or list known vaults from `tokens/*.meta.json`.

### Add a vault (multi-token install)

When the user gives you a triple — *KB slug, token, label* — pipe the token into `cb-add-vault`. The script reads stdin, validates against `/api/me`, and writes `tokens/<slug>` (mode 0600) + `tokens/<slug>.meta.json`:

```bash
echo "<TOKEN>" | $SBD/bin/cb-add-vault <SLUG> "<LABEL>"
# → { "ok": true, "label": "...", "kb_slug": "...", "kb_name": "..." }
```

The token literal still flows through your context **once** because the user just gave it to you. After this call, every other operation reads the token from disk via the `cb-*` scripts and the secret stays out of your context.

If the script reports 401 / mismatch, it cleans up after itself; report the failure to the user and ask for a fresh token.

### List configured vaults

When the user asks *"which vaults do I have?"*:

```bash
$SBD/bin/cb-list-vaults    # JSON: { vaults: [{label, kb_slug}] }
```

Cross-check by running `cb-list-kbs` and comparing slugs — flag any local meta whose KB no longer exists.

Bash fallback (covers the new layout + legacy paths):

```bash
# Pick token by KB slug if one is in play; else fall back to default.
KB="${TARGET_KB:-default}"
SBD="$HOME/.claude/skills/heivol-contextbox"
T="${HEIVOL_CONTEXTBOX_TOKEN:-$(
  cat "$SBD/tokens/$KB" 2>/dev/null \
  || cat "$SBD/tokens/default" 2>/dev/null \
  || cat "$SBD/token" 2>/dev/null \
  || cat ~/.config/contextbox/token 2>/dev/null
)}"
H="authorization: Bearer $T"
```

**On first call, GET `/api/me`** to discover the active token's scope. The response carries `token.kb_id` (set when KB-scoped) — if it's set and you need a different KB, switch tokens. Cross-KB calls with a KB-scoped token return 404 (the other KB is invisible).

If no token resolves, send the user to `https://contextbox.heivol.com/install` for a fresh bundle. **Do not say "no token" until you have inspected `<base>/tokens/`, `<base>/token`, and the env var.**

User bearer for `/api/*`. Admin bearer (`~/.config/contextbox/admin-token`) only for `/api/admin/*`.

### Why per-KB tokens

When you're a member of a KB you don't own (someone shared it with you), a KB-scoped token lets you carry the *minimum* trust for that KB. If the device holding it is lost or the token leaks, the blast radius is exactly that one KB — your own KBs and any other shared KBs are untouched.

For everyday work, `tokens/default` is enough. Reach for KB-scoped tokens when shared KBs are sensitive (someone else's family photos, a teammate's strategy notes), or when an automation only needs one KB.

## Navigation — same patterns as the Obsidian vault

Treat your KBs like a vault. Same primitives, same order, same discipline.

| Vault primitive                        | Contextbox primitive                                            |
| -------------------------------------- | --------------------------------------------------------------- |
| `vault-search "<q>"`                   | `POST /api/search { q }` — spans every KB you can read          |
| `vault-search "<q>" --scope <subtree>` | `POST /api/search { q, kb_slug }` — narrow to one KB            |
| `vault-search "<q>" --limit N`         | `POST /api/search { q, limit }`                                 |
| `glob projects/foo/**`                 | `GET /api/kbs/<slug>/docs?prefix=foo/&limit=…`                  |
| Read a file at a path                  | `GET /api/kbs/<slug>/docs/<path>`                               |
| `vault-search --reindex`               | not needed — index updates on PUT                               |

**MANDATORY: search first.** Same rule as the vault — when the question could plausibly be in the user's notes, call `search` BEFORE answering from general knowledge, BEFORE listing docs, BEFORE asking the user. `list_docs` is a fallback for enumeration ("how many recipes under family/2026/"), never the first move for content questions.

**Order of operations:**

1. **Broad search first.** Omit `kb_slug` — the search spans every KB you're a member of. Cheap, high-recall.
2. **Narrow with `kb_slug`** once a KB clearly has the answer. Equivalent to `--scope` in the vault.
3. **Drill into `read_doc`** only when the snippet doesn't carry enough context.
4. **Cite inline** as `[snippet](citation_url)`. Every result ships a `citation_url` — use it.
5. **Verify retrievability after PUT.** Re-search with distinctive keywords before reporting an upload done. Same shape as the vault's `--reindex` + verify dance.

Result shape: `{ results: [{ kb_slug, doc_path, chunk_idx, heading, snippet, score, citation_url }] }` — scored snippets, same mental model as vault search hits.

## Read

Use the scripts:

```bash
$SBD/bin/cb-search "<query>"                       # spans every reachable KB
$SBD/bin/cb-search "<query>" --kb=<slug>           # narrow to one KB (--scope)
$SBD/bin/cb-list-kbs                               # JSON: kbs[].permissions.{read,write}
$SBD/bin/cb-list-docs <slug> --prefix=foo/ --limit=50
$SBD/bin/cb-read-doc <slug> <path>
$SBD/bin/cb-me                                     # confirm caller + scope
```

Gate uploads on `kb.permissions.write` — saves a 403 round-trip when the token is read-only. Write access requires role ≥ editor AND token scope includes write; everything returned is at least readable.

## Write — KBs and docs (write scope; editor+ on KB)

```bash
# create_kb — caller becomes owner; server generates the slug (nanoid21).
# tenant_slug optional if token resolves a single tenant. name optional.
curl -sS -X POST "$API/api/kbs" -K <(printf 'header = "authorization: Bearer %s"\n' "$(cat $SBD/tokens/default)") \
  -H "content-type: application/json" -d '{"tenant_slug":"<tenant>","name":"<Display>"}'
# → response.kb.slug is the server-generated nanoid21

$SBD/bin/cb-upload-doc <slug> <path> <local-file.md>   # PUT (re-PUT to update)
# delete_doc: same auth + DELETE on /api/kbs/<slug>/docs/<path>
```

`create_kb` and `delete_kb` don't have dedicated scripts yet — use raw curl with the token-file pattern above (read once, no echo).

### Git history is the audit log

Every successful PUT and DELETE is mirrored as a commit in the KB's bare repo (`/var/lib/contextbox/git/<tenant>/<kb>.git`, author `contextbox-api`). Same-content PUTs are no-ops — no empty commit is created. Use the KB's git URL when you need:

- "What changed since X?" — `git log --since=...`
- "Who/what touched this doc?" — `git log -- <path>`
- "Diff of this PUT" — `git show <commit>`

Don't hand-write a `_log.md` or `activity-log.md` per KB — git history is the canonical record. KB hubs (`_index.md`) are still useful for discoverability and serve a different purpose.

### Splitting large docs (rule of thumb: 500 lines)

When you write or modify a doc and it crosses **~500 lines**, look for a clean split before uploading. Big docs hurt retrieval (chunks lose locality), are slow to read end-to-end, and tend to bury sub-topics that callers want to cite independently. Skip the split only when the content is genuinely indivisible (a single tabular reference, a long log, an entry that loses meaning if cut).

How to split well:

- Find natural seams — top-level `##` sections that stand on their own (e.g. `recommendations-japan.md` vs `recommendations-philippines.md`, or `budget.md` carved out of an itinerary).
- Keep the **parent doc** as the index/overview. Replace the moved section with a one-line pointer + wiki-link (`[[<kb-path>/<sibling>]]`).
- Add `parent: "[[<parent-path>]]"` frontmatter to each child so backlinks are explicit.
- Re-upload all touched docs; the KB re-indexes on PUT.
- Verify with `cb-search` against a distinctive keyword from the moved content to confirm the split landed.

## Sharing — invites and share links (owner only)

```bash
# email invite — recipient sees a banner on next sign-in; accept is idempotent
curl -sS -X POST https://contextbox.heivol.com/api/kbs/<slug>/shares -H "$H" -H "content-type: application/json" \
  -d '{"recipient_email":"alice@example.com","role":"reader"}'

# share link — anyone who can sign in to HeivolID may join until expiry/max_uses
curl -sS -X POST https://contextbox.heivol.com/api/kbs/<slug>/share-links -H "$H" -H "content-type: application/json" \
  -d '{"role":"editor","expires_in_hours":72,"max_uses":5}'
# → { url: "https://contextbox.heivol.com/share/<token>" }
# Share links can never grant owner.

# list / revoke
curl -sS https://contextbox.heivol.com/api/kbs/<slug>/shares -H "$H"
curl -sS -X DELETE https://contextbox.heivol.com/api/kbs/<slug>/shares/<id> -H "$H"
curl -sS https://contextbox.heivol.com/api/kbs/<slug>/share-links -H "$H"
curl -sS -X DELETE https://contextbox.heivol.com/api/kbs/<slug>/share-links/<id> -H "$H"
```

## Member management (owner only)

Last owner can never be removed/demoted.

```bash
curl -sS https://contextbox.heivol.com/api/kbs/<slug>/members -H "$H"
curl -sS -X PATCH https://contextbox.heivol.com/api/kbs/<slug>/members/<user_id> -H "$H" -H "content-type: application/json" \
  -d '{"role":"owner|editor|reader"}'
curl -sS -X DELETE https://contextbox.heivol.com/api/kbs/<slug>/members/<user_id> -H "$H"
```

## My invites + tokens

```bash
curl -sS https://contextbox.heivol.com/api/me/invites -H "$H"
curl -sS -X POST https://contextbox.heivol.com/api/invites/<id>/accept -H "$H"
curl -sS -X POST https://contextbox.heivol.com/api/invites/<id>/reject -H "$H"

curl -sS https://contextbox.heivol.com/api/me/tokens -H "$H"
curl -sS -X DELETE https://contextbox.heivol.com/api/me/tokens/<id> -H "$H"

# Rename (PATCH label) via script:
$SBD/bin/cb-rename-token <id> "<new label>"
```

**Mint a new user token** — requires a signed-in session cookie (sign in at `/install`); 5/hour per user. `kb_slug` (optional) binds the token to one KB:

```bash
curl -sS -X POST https://contextbox.heivol.com/api/me/tokens \
  --cookie "contextbox_session=<session>" -H "content-type: application/json" \
  -d '{"scope":["read","write"],"label":"laptop","expires_in_days":365,"kb_slug":"family-recipes"}'
# omit kb_slug for a user-scoped token (default — covers every KB you can reach).
```

**Rotate** (refresh-and-revoke; same label/scope/kb_id):

```bash
curl -sS -X POST https://contextbox.heivol.com/api/me/tokens/<id>/rotate \
  --cookie "contextbox_session=<session>"
# old token immediately revoked; response carries new secret + a one-shot
# /install/skill.zip?secret_id=... URL.
```

Easier: open `https://contextbox.heivol.com/install`, sign in, click **+ New token** or **Refresh & download** on an existing row — the bundle downloads automatically. Each download adds one token to `tokens/` without overwriting existing ones, so layering a KB-scoped token on top of an existing user-scoped install is a single click. Extract: `unzip -o ~/Downloads/heivol-contextbox-skill.zip -d ~/.claude/skills/`. If macOS Archive Utility extracted instead of `unzip`, run `chmod 600 ~/.claude/skills/heivol-contextbox/tokens/*` and recreate the symlink (`cd ~/.claude/skills/heivol-contextbox && rm -f token && ln -s tokens/default token`).

## Admin (admin bearer only)

Cross-user / cross-tenant ops. Bearer in `~/.config/contextbox/admin-token`; on host at `/etc/contextbox/env` on CT 205 (`ssh proxmox-heivol "pct enter 205"`).

```bash
A=$(cat ~/.config/contextbox/admin-token); AH="authorization: Bearer $A"

curl -sS -X POST https://contextbox.heivol.com/api/admin/tenants -H "$AH" -H "content-type: application/json" \
  -d '{"slug":"...","name":"..."}'

curl -sS -X POST https://contextbox.heivol.com/api/admin/kbs -H "$AH" -H "content-type: application/json" \
  -d '{"tenant_slug":"...","slug":"...","name":"..."}'                                # bypasses caller membership

curl -sS -X POST https://contextbox.heivol.com/api/admin/tokens -H "$AH" -H "content-type: application/json" \
  -d '{"tenant_slug":"<tenant>","scope":"read","kb_slug":"<optional>","label":"<purpose>"}'

curl -sS https://contextbox.heivol.com/api/admin/tenants/<tenant>/members -H "$AH"
curl -sS -X POST https://contextbox.heivol.com/api/admin/tenants/<tenant>/members -H "$AH" -H "content-type: application/json" \
  -d '{"user_id":"<uuid>","role":"admin|member"}'
curl -sS -X DELETE https://contextbox.heivol.com/api/admin/tenants/<tenant>/members/<user_id> -H "$AH"

curl -sS https://contextbox.heivol.com/api/admin/users -H "$AH"
curl -sS https://contextbox.heivol.com/api/admin/users/<id>/tenants -H "$AH"
```

## Errors

- `401` — token revoked / no session. Mint a new one.
- `403` — role too low for this action.
- `404` — KB doesn't exist OR you're not a member (cross-tenant existence never leaked).
- `410` — share link / invite expired-revoked-exhausted, or invite already accepted.
- `429` — `/api/me/tokens` mint rate limit (5/hr per user).

