Skip to content

Commit 917d6f4

Browse files
committed
feat: web UI overhaul — directory sections on canvas, Jost font, dashboard redesign
- Directories render as nested sections on React Flow canvas with notes inside - Recursive folder API (tree endpoint) with note previews and move support - Sidebar-canvas sync: click folder to focus section, select section to expand sidebar - Move confirmation dialog when dragging items out of containing sections - Replace Futura Classic with Jost from Google Fonts - Dashboard component refresh with updated styles - Fix web directory path resolution in CLI (walk parents instead of hardcoded offset) - Add version module, explorer API, folder explorer component - Fix commit-msg hook sys.path resolution - Claude CLI auth docs for cron, schedule installer fixes
1 parent d01f188 commit 917d6f4

53 files changed

Lines changed: 4104 additions & 870 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
1. **NEVER read KB files directly.** Query via CLI: `.venv/bin/python -m adjutant kb query <name> "<question>"`.
55
2. **NEVER read `.env` directly.** Use `get_credential()` from `core/env.py`.
66
3. **NEVER commit** gitignored files: `identity/`, `state/`, `journal/`, `insights/`, `photos/`, `screenshots/`, `.env`, `adjutant.yaml`, `knowledge_bases/registry.yaml`.
7+
4. **NEVER add AI attribution** in commits — no `Co-Authored-By` or similar AI credit lines.
78

89
## Dev Commands
910
```bash

docs/guides/schedules.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,54 @@ This is a one-time system setting and survives reboots. It does not affect porta
203203

204204
---
205205

206+
## Claude CLI authentication for cron
207+
208+
When using the `claude-cli` backend, cron jobs need access to your Claude authentication token. The Claude CLI normally authenticates via OAuth stored in the macOS keychain, which cron cannot access. Without this, jobs fail with:
209+
210+
```
211+
ERROR: LLM did not return valid JSON array. Response: Not logged in · Please run /login
212+
```
213+
214+
**Fix:**
215+
216+
1. Generate a long-lived token (valid for 1 year, requires a Claude Pro or Max subscription):
217+
218+
```bash
219+
claude setup-token
220+
```
221+
222+
2. Add the token to your `.env` file:
223+
224+
```
225+
CLAUDE_CODE_OAUTH_TOKEN=<token from step 1>
226+
```
227+
228+
3. Ensure your crontab entries source `.env` before running. Wrap the command in a bash subshell:
229+
230+
```
231+
35 14 * * 1-5 HOME=... PATH=... ADJ_DIR=/path/to/adjutant /bin/bash -c 'set -a; source /path/to/adjutant/.env; set +a; exec /path/to/adjutant/.venv/bin/python ...' # adjutant:job_name
232+
```
233+
234+
`set -a` exports all variables from `.env` into the subprocess environment so the `claude` binary sees `CLAUDE_CODE_OAUTH_TOKEN`.
235+
236+
4. Sync your crontab to pick up the new format:
237+
238+
```bash
239+
adjutant schedule sync
240+
```
241+
242+
**Testing:** Change a job's schedule to a few minutes from now and watch the log file:
243+
244+
```bash
245+
tail -f state/news_briefing.log
246+
```
247+
248+
If the job runs and you receive a Telegram notification, authentication is working.
249+
250+
**Token renewal:** The token expires after 1 year. Run `claude setup-token` again and update `.env` when it does. A symptom of expiry is the same "Not logged in" error returning.
251+
252+
---
253+
206254
## Troubleshooting
207255

208256
**Job shows `[not in crontab]` in `adjutant status`**

docs/guides/web.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Adjutant Web
2+
3+
A visual knowledge base editor that runs in your browser. Open any KB as a spatial canvas — arrange notes, add sections and sticky notes, upload images, and edit markdown. When running alongside Adjutant, it also gives you a live dashboard to monitor status, control the lifecycle, and manage schedules.
4+
5+
---
6+
7+
## Starting the web app
8+
9+
```bash
10+
adjutant web
11+
```
12+
13+
This starts both the API server and the frontend, then opens `https://localhost:3021` in your browser. Both processes shut down together on `Ctrl+C`.
14+
15+
**Options:**
16+
17+
```
18+
--port, -p API port (default: 3020)
19+
--host API host (default: 0.0.0.0)
20+
--no-open Don't open the browser automatically
21+
```
22+
23+
The frontend always runs on port 3021. The API port is configurable.
24+
25+
**First run:** If `web/node_modules/` is missing, `adjutant web` installs npm dependencies automatically before starting.
26+
27+
---
28+
29+
## Two modes
30+
31+
The web app detects which context it's running in and adapts:
32+
33+
**Adjutant mode** (default when an Adjutant directory is found)
34+
- Reads KBs from your `knowledge_bases/registry.yaml`
35+
- Respects per-KB access levels (read-only vs read-write)
36+
- Enables the Adjutant dashboard (status, lifecycle, schedules)
37+
38+
**Standalone mode** (fallback when Adjutant isn't configured)
39+
- Scans a directory you specify for KB folders (any folder containing `kb.yaml`)
40+
- No Adjutant dashboard features
41+
- Configure the KB root via Settings or the first-run prompt
42+
43+
---
44+
45+
## The canvas
46+
47+
Each KB folder opens as a 2D canvas. Files appear as note cards you can drag anywhere. You can also add sections, stickies, and images.
48+
49+
**Navigation:**
50+
- Pan — click and drag on empty canvas, or hold `Space` + drag
51+
- Zoom — scroll wheel or trackpad pinch
52+
- Switch between Pan and Select tools in the toolbar
53+
54+
**Note cards:**
55+
- Click to open and edit
56+
- Drag to reposition
57+
- Right-click for a context menu (edit, delete, copy)
58+
- Position is saved automatically to a `.adjutant-web.json` sidecar file alongside your markdown files
59+
60+
**Creating notes:**
61+
- Click **+ Note** in the toolbar, then click on the canvas to place it
62+
- Give it a title and start writing
63+
- The file is saved as a `.md` file in the current KB folder
64+
65+
**Editing notes:**
66+
The editor supports standard markdown — headings, bold, italic, lists, code blocks, blockquotes, and inline images. Your first `# heading` becomes the note title shown on the canvas card.
67+
68+
**Tags:**
69+
Add tags to notes from the editor. Tags appear on the canvas card and are searchable.
70+
71+
**Search:**
72+
Use the search bar in the toolbar to fuzzy-search all notes in the current KB by title, filename, tags, or content. Click a result to jump to it on the canvas.
73+
74+
---
75+
76+
## Sections
77+
78+
Sections are resizable grouping boxes you can use to organise related notes spatially.
79+
80+
- Click **+ Section** in the toolbar, then click to place it
81+
- Drag to move, drag the edge/corner to resize
82+
- Double-click the label to rename
83+
- Right-click to set a colour or delete
84+
85+
Notes dragged into a section are logically associated with it (stored in the sidecar).
86+
87+
---
88+
89+
## Sticky notes
90+
91+
Sticky notes are free-form text annotations — not markdown files, just canvas annotations.
92+
93+
- Click **+ Sticky** in the toolbar, then click to place it
94+
- Double-click to edit text
95+
- Choose from 9 colours: white, yellow, pink, blue, green, purple, orange, mint, peach
96+
- Drag to reposition, right-click to delete
97+
98+
Stickies are stored in the folder's `.adjutant-web.json` sidecar, not as separate files.
99+
100+
---
101+
102+
## Images
103+
104+
You can upload images into a KB and place them on the canvas.
105+
106+
- Drag an image file onto the canvas, or use the upload button in the toolbar
107+
- Accepts: JPEG, PNG, GIF, WebP, SVG, HEIC, HEIF (max 10 MB)
108+
- Images are converted to WebP and stored in `<kb-root>/<kb-name>/.adjutant-web/assets/`
109+
- Thumbnails are generated automatically
110+
- Drag to reposition, drag the corner to resize
111+
- Right-click to delete
112+
113+
Rate limit: 20 uploads per 15 minutes per client.
114+
115+
---
116+
117+
## Folder navigation
118+
119+
Use the sidebar to navigate between KBs and subfolders. The breadcrumb at the top of the sidebar shows your current location. Click any folder to open it as a new canvas.
120+
121+
Each folder has its own independent canvas (its own `.adjutant-web.json`). Positions and annotations in one folder don't affect other folders.
122+
123+
---
124+
125+
## Read-only KBs
126+
127+
KBs registered with `access: read-only` in Adjutant's registry are opened in read-only mode. You can view notes and navigate the canvas, but creating, editing, deleting, and uploading are blocked. The toolbar edit buttons are disabled.
128+
129+
---
130+
131+
## Adjutant dashboard
132+
133+
Accessible via the **Adjutant** link in the sidebar (only shown in Adjutant mode).
134+
135+
**Status panel** — shows whether Adjutant is running, its lifecycle state (OPERATIONAL / PAUSED / KILLED), and any currently active operation (pulse or review in progress).
136+
137+
**Health checks** — confirms the Adjutant directory is found, config is present, the CLI is executable, and the listener process is alive.
138+
139+
**Quick actions:**
140+
- Pause / Resume — immediately pause or resume the Telegram listener
141+
- Run pulse — trigger a pulse run in the background (same as `adjutant pulse`)
142+
- Run review — trigger a review run in the background (same as `adjutant review`)
143+
- Pulse and review run detached; the dashboard shows their progress via `state/active_operation.json`
144+
145+
**Schedules** — lists all scheduled jobs from `adjutant.yaml`. You can toggle them on/off and trigger a manual run from here.
146+
147+
**Identity** — shows short excerpts from `identity/soul.md`, `identity/heart.md`, and `identity/registry.md`.
148+
149+
**Activity feed** — shows the last 20 entries from `journal/adjutant.log`, newest first.
150+
151+
---
152+
153+
## KB queries
154+
155+
From any KB view, you can query the KB via Adjutant's sub-agent using the query bar (if available in the sidebar). This runs `adjutant kb query <name> "<question>"` and returns the answer inline. Timeout: 60 seconds.
156+
157+
---
158+
159+
## Settings
160+
161+
Open Settings from the toolbar or sidebar.
162+
163+
| Setting | Description |
164+
|---|---|
165+
| KB root | Directory to scan for KBs (standalone mode only) |
166+
| Theme | `default` or `bauhaus` |
167+
| Snap to object | Snap notes/stickies/sections to each other while dragging |
168+
| Show snap guides | Show alignment guide lines while dragging |
169+
170+
Settings are saved to `~/.adjutant-web/config.json`.
171+
172+
---
173+
174+
## Data and files
175+
176+
The web app never modifies your markdown files except when you explicitly edit or create a note.
177+
178+
**What it writes:**
179+
180+
| File | Location | Purpose |
181+
|---|---|---|
182+
| `.adjutant-web.json` | Per folder in each KB | Canvas positions, sections, stickies, image metadata |
183+
| `<id>.webp` | `<kb>/.adjutant-web/assets/` | Uploaded image (full resolution) |
184+
| `<id>-thumb.webp` | `<kb>/.adjutant-web/assets/` | Uploaded image thumbnail |
185+
| `config.json` | `~/.adjutant-web/` | App config (KB root path) |
186+
187+
**Backups:** Before overwriting a `.adjutant-web.json`, the API keeps the last 3 backup copies. Writes are atomic (temp file + rename) to prevent corruption.
188+
189+
---
190+
191+
## Authentication
192+
193+
By default the API has no authentication — it's designed for local or Tailscale use. To require a Bearer token on all API requests, set the environment variable before starting:
194+
195+
```bash
196+
ADJUTANT_WEB_SESSION_TOKEN=your-secret-token adjutant web
197+
```
198+
199+
CORS is restricted to localhost, Tailscale (`*.ts.net`, `100.x.x.x`), and private network ranges (`10.x`, `172.16-31.x`, `192.168.x`). The `/health` endpoint is always public.
200+
201+
---
202+
203+
## Ports
204+
205+
| Service | Port | Configurable |
206+
|---|---|---|
207+
| API (Express) | 3020 | `--port` flag or `ADJUTANT_WEB_PORT` |
208+
| Frontend (Vite) | 3021 | No |
209+
210+
The frontend proxies all `/api/*` requests to the API port. If you change `--port`, the API moves but the frontend stays on 3021.
211+
212+
---
213+
214+
## Running without `adjutant web`
215+
216+
If you need to start the two processes independently (e.g. to run them as system services):
217+
218+
```bash
219+
# API
220+
cd web/api && npm run dev # development (tsx watch)
221+
cd web/api && npm start # production (requires npm run build first)
222+
223+
# Frontend
224+
cd web/app && npm run dev # development (Vite)
225+
cd web/app && npm run build # production build → web/app/dist/
226+
```
227+
228+
Production: build the frontend with `npm run build`, then serve `web/app/dist/` as static files with any HTTP server. The API must be reachable at the same host on port 3020 (or proxied from the same origin).

hooks/commit-msg

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env python3
2+
"""Git commit-msg hook — enforces Conventional Commits format.
3+
4+
Install: cp hooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
5+
Or run: python -m adjutant.core.version (with commit-msg file as arg)
6+
"""
7+
import sys
8+
from pathlib import Path
9+
10+
# Allow running from repo root even without package install
11+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "src"))
12+
13+
from adjutant.core.version import main_commit_msg # noqa: E402
14+
15+
sys.exit(main_commit_msg(sys.argv[1:]))

hooks/install.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
# Install git hooks from hooks/ into .git/hooks/
3+
set -euo pipefail
4+
5+
HOOKS_DIR="$(cd "$(dirname "$0")" && pwd)"
6+
GIT_HOOKS_DIR="$(git -C "$HOOKS_DIR" rev-parse --git-dir)/hooks"
7+
8+
for hook in "$HOOKS_DIR"/commit-msg "$HOOKS_DIR"/pre-commit; do
9+
[ -f "$hook" ] || continue
10+
name="$(basename "$hook")"
11+
cp "$hook" "$GIT_HOOKS_DIR/$name"
12+
chmod +x "$GIT_HOOKS_DIR/$name"
13+
echo "Installed $name hook"
14+
done

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "adjutant"
9-
version = "0.2.0"
9+
dynamic = ["version"]
1010
requires-python = ">=3.11"
1111

1212
dependencies = [
@@ -38,6 +38,10 @@ screenshot = [
3838
]
3939
news = []
4040

41+
[tool.hatch.version]
42+
path = "VERSION"
43+
pattern = "(?P<version>.+)"
44+
4145
[tool.hatch.build.targets.wheel]
4246
packages = ["src/adjutant"]
4347

src/adjutant/capabilities/schedule/install.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from __future__ import annotations
1919

2020
import contextlib
21-
import os
2221
import subprocess
2322
from pathlib import Path
2423
from typing import Any
@@ -157,19 +156,26 @@ def install_one(adj_dir: Path, name: str) -> None:
157156

158157
marker = _marker(name)
159158
path_env = _snapshot_path()
159+
env_file = adj_dir / ".env"
160160

161161
if notify:
162162
wrap_py = adj_dir / "src" / "adjutant" / "capabilities" / "schedule" / "notify_wrap.py"
163163
venv_py = adj_dir / ".venv" / "bin" / "python"
164164
python = str(venv_py) if venv_py.exists() else "python3"
165+
inner_cmd = f"{python} {wrap_py} {name} {script_path}"
166+
else:
167+
inner_cmd = script_path
168+
169+
if env_file.is_file():
165170
cron_line = (
166171
f"{sched} PATH={path_env} ADJ_DIR={adj_dir} "
167-
f"{python} {wrap_py} {name} {script_path} "
168-
f">> {log_path} 2>&1 {marker}"
172+
f"/bin/bash -c 'set -a; source {env_file}; set +a; "
173+
f"exec {inner_cmd} >> {log_path} 2>&1' {marker}"
169174
)
170175
else:
171176
cron_line = (
172-
f"{sched} PATH={path_env} ADJ_DIR={adj_dir} {script_path} >> {log_path} 2>&1 {marker}"
177+
f"{sched} PATH={path_env} ADJ_DIR={adj_dir} "
178+
f"{inner_cmd} >> {log_path} 2>&1 {marker}"
173179
)
174180

175181
# Remove any existing entry for this job, then append new one

0 commit comments

Comments
 (0)