Spaces:
Running on Zero
Running on Zero
File size: 12,164 Bytes
6f9a5fd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | # M12 β CLI
**Spec version:** v1.0
**Depends on:** X04 (config), M01 (identity), M03 (bus, via IPC), X03 (observability for doctor), `click`
**Depended on by:** Users; packaging
---
## 1. Responsibility
Provide the `hearthnet` command. Each subcommand is small, scriptable, exit-code-correct. The CLI either:
- Runs in **standalone mode**: does not need a running node (init, doctor on cold disk, export, erase)
- Talks to a **running node** over local HTTP (`status`, `caps`, `log`, `trace`), bypassing the UI
The CLI never imports a service module. For node-state queries it uses the bus's HTTP API on `127.0.0.1:7080` like any other client.
---
## 2. File layout
```
hearthnet/
βββ cli.py # Click app, all subcommands
βββ __main__.py # `python -m hearthnet` β cli.main()
βββ doctor.py # re-export from X03 for `hearthnet doctor`
```
Installed as console script in `pyproject.toml`:
```toml
[project.scripts]
hearthnet = "hearthnet.cli:main"
```
---
## 3. Subcommands
### 3.1 `hearthnet init`
```
hearthnet init [--name NAME] [--profile PROFILE] [--non-interactive]
```
Bootstraps a new node:
1. Resolves XDG paths, creates dirs
2. Generates keypair if absent (M01)
3. Writes default `config.toml`
4. Interactive prompts (unless `--non-interactive`):
- Display name
- Profile (auto-detected from hardware)
- Create or join community
5. If create: builds genesis community manifest, writes it, prints invite QR to terminal (Unicode block art) and saves PNG
6. If join: prompts for invite text, redeems
Exits 0 on success, 2 on user abort, 1 on error.
### 3.2 `hearthnet run`
```
hearthnet run [--config PATH] [--no-ui] [--debug]
```
Starts the node:
1. Loads config (X04)
2. Configures observability (X03)
3. Loads keypair (M01) β refuses if missing
4. Verifies community manifest present β if not, redirects to init
5. Composes the node (see [`node.py` in the package layout](#5-the-orchestrator-nodepy))
6. Blocks until SIGINT / SIGTERM
`--no-ui` skips Gradio (useful for headless anchor / RPi).
`--debug` raises log level to debug.
### 3.3 `hearthnet status`
```
hearthnet status [--json]
```
Connects to local node at `127.0.0.1:7080`. Reports:
- Our node ID + display name + profile
- Community ID + name + member count
- Online state (online/degraded/offline) + duration in this state
- Peers visible (count + summaries)
- Registered local capabilities (count + names)
- In-flight calls
- Event log head Lamport
- Disk usage (blobs + events)
Exits 0 if reachable, 3 if not reachable, 1 on bad response.
### 3.4 `hearthnet caps`
```
hearthnet caps [--remote-only | --local-only] [--name PATTERN]
```
Lists capability entries. Columns: `name`, `version`, `stability`, `node`, `model/params`, `health`, `p50ms`, `in_flight`.
### 3.5 `hearthnet call`
```
hearthnet call NAME[@VERSION] --body '<json>' [--stream]
```
Make a one-shot capability call. Useful for scripting and testing.
```
hearthnet call llm.chat@1.0 --stream \
--body '{"params":{"model":"qwen2.5-7b-instruct"},"input":{"messages":[{"role":"user","content":"Hi"}]}}'
```
Streams to stdout. Non-zero exit code reflects wire error code (mapped: see [CONTRACT Β§9](../CAPABILITY_CONTRACT.md)).
### 3.6 `hearthnet log`
```
hearthnet log [--follow] [--level LEVEL] [--component NAME]
```
Tails the structured log file. With `--follow`, behaves like `tail -F` and filters live.
### 3.7 `hearthnet trace`
```
hearthnet trace recent [N] [--capability NAME]
```
Pulls the trace ring buffer via `/trace/recent`. Pretty-prints last N traces.
### 3.8 `hearthnet doctor`
```
hearthnet doctor [--check NAME]
```
Runs X03's self-diagnostics (`run_all` or `run_one`). Coloured terminal output:
```
β keys_present /home/christof/.local/share/hearthnet/keys/device.ed25519
β keys_loadable Ed25519, 32 bytes
β mdns_socket Port 5353 in use by avahi-daemon
β fix: sudo systemctl stop avahi-daemon
...
```
Exit code: 0 if all pass, 1 if any fail, 2 if doctor itself crashed.
### 3.9 `hearthnet export`
```
hearthnet export [--out PATH]
```
Exports all local data for this user (GDPR right-to-export):
- Public manifest
- Our authored events
- Our chat history
- Our pinned files (CIDs + filenames)
- Our marketplace posts
- Settings (without secrets)
Output: a signed ZIP at `<PATH>` (default `~/hearthnet-export-<date>.zip`).
### 3.10 `hearthnet erase`
```
hearthnet erase [--keep-keys] [--yes]
```
Erases local state. Prompts thrice. With `--keep-keys`, retains the device key (allowing rejoin later).
Order of erase:
1. Stop running node (best-effort over IPC)
2. Wipe `<DATA>/communities/<id>/` (events, manifests, snapshots)
3. Wipe `<DATA>/blobs/` (unless pinned with `--keep-blobs`)
4. Wipe `<CACHE>/embeddings/`
5. Wipe `<LOG>` (unless `--keep-logs`)
6. Wipe `<DATA>/keys/` unless `--keep-keys`
7. Print summary
### 3.11 `hearthnet rag`
```
hearthnet rag list
hearthnet rag ingest PATH --corpus NAME
hearthnet rag reindex --corpus NAME [--embedding-model MODEL]
```
Local CLI for RAG operations. Calls `rag.list_corpora`, `rag.ingest`, and (for reindex) a privileged local-only flow that re-embeds an existing corpus.
### 3.12 `hearthnet invite`
```
hearthnet invite create --node-id NODEID --level LEVEL --ttl HOURS
hearthnet invite redeem TEXT_OR_PATH
```
CLI equivalents of the M13 onboarding flows. Useful for headless anchors.
### 3.13 `hearthnet version`
Prints `__version__`, contract version, Python version, OS. One line.
---
## 4. CLI architecture (Click)
```python
# hearthnet/cli.py
import click
@click.group()
@click.option("--config", type=click.Path(), help="Path to config.toml")
@click.pass_context
def main(ctx, config):
ctx.obj = load_config(Path(config) if config else None)
@main.command()
@click.option("--name")
...
def init(...): ...
# ... etc. Each subcommand is its own function.
```
Each command function is < 40 lines and delegates to module-level helpers in the same file. Tests can call the helpers directly without invoking Click runtime.
---
## 5. The orchestrator (`node.py`)
The CLI's `run` subcommand calls into `hearthnet.node.start`. This is not strictly part of M12 but is documented here for completeness because it's the central wiring point.
```python
# hearthnet/node.py
async def start(config: Config) -> None:
# 1. observability
observability.logging.configure(config.observability)
observability.metrics.configure(config.observability)
# 2. identity
kp = identity.keys.load_or_generate(config.identity.keys_dir)
# 3. community check (M13 redirect if missing)
if config.community.community_id is None:
await onboarding.run_blocking(config, kp) # writes config; restart cycle
return
# 4. core state
event_log = events.EventLog(config.community.state_dir / "events.sqlite",
config.community.community_id)
snapshot_store = events.SnapshotStore(config.community.state_dir / "snapshots",
config.community.community_id)
replay_engine = events.ReplayEngine(event_log)
community_manifest = identity.manifest.load_or_regenerate(...)
# 5. blobs
blob_store = blobs.BlobStore(config.file.blobs_dir, gc_threshold=config.file.gc_threshold)
# 6. transport + bus
pinned = transport.PinnedCerts(...)
http_client = transport.HttpClient(kp, kp.node_id_full, config.community.community_id, pinned)
bus = CapabilityBus(kp.node_id_full, config.community.community_id, config.bus,
http_client, lambda: community_manifest)
# 7. peer registry + discovery
peer_registry = discovery.PeerRegistry(kp.node_id_full, config.community.community_id)
mdns_announcer = discovery.MdnsAnnouncer(...)
mdns_browser = discovery.MdnsBrowser(peer_registry, config.community.community_id)
udp_announcer = discovery.UdpAnnouncer(...)
udp_listener = discovery.UdpListener(peer_registry, config.community.community_id)
# 8. services
services_list = []
if config.embedding:
services_list.append(EmbeddingService(config.embedding))
if config.llm.backends:
services_list.append(LlmService(config.llm))
if config.rag.enabled:
services_list.append(RagService(config.rag, bus, blob_store, event_log, lambda: community_manifest))
if config.file:
services_list.append(FileService(config.file, blob_store, event_log))
if config.market.enabled:
services_list.append(MarketplaceService(config.market, bus, event_log, replay_engine, kp,
lambda: community_manifest))
if config.chat.enabled:
services_list.append(ChatService(config.chat, bus, event_log, replay_engine, peer_registry, kp,
kp.node_id_full))
for s in services_list:
bus.register_service(s)
await s.start()
# 9. emergency detector
state_bus = emergency.StateBus()
detector = emergency.Detector(config.emergency, bus, state_bus)
# 10. transport server
http_server = transport.HttpServer(config.transport, kp, bus,
event_sync=events.SyncServer(event_log),
community_manifest_provider=lambda: community_manifest)
# 11. UI
ui_app = ui.build_ui(bus, state_bus, config.ui,
node_id_short=kp.node_id_short, community_name=community_manifest.name)
# 12. wire peer events β bus
peer_registry.subscribe(...).on_event(bus.on_peer_added, bus.on_peer_updated, bus.on_peer_removed)
# 13. periodic manifest publish
publisher = ManifestPublisher(kp, community_manifest_provider=..., bus=bus,
peer_registry=peer_registry, interval_seconds=MANIFEST_REPUBLISH_INTERVAL_SECONDS)
# 14. periodic sync
syncer = events.SyncClient(event_log, http_client)
sync_loop = PeriodicTask(lambda: syncer.run_round(peer_registry), interval_seconds=300)
# 15. run everything
await asyncio.gather(
http_server.run(),
mdns_announcer.start(), mdns_browser.start(),
udp_announcer.run(), udp_listener.run(),
detector.run(),
publisher.run(),
sync_loop.run(),
ui_app.launch_async(),
)
```
This is the canonical wiring. Anything that looks different across modules is wrong.
---
## 6. Exit code reference
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Generic error (see stderr) |
| 2 | User aborted / bad usage |
| 3 | No running node (for commands needing IPC) |
| 4 | Auth / signature failure |
| 5 | Disk full / capacity exceeded |
---
## 7. Configuration
The CLI reads the same `config.toml` as the daemon. `--config` overrides the path.
---
## 8. Tests
### Unit (per subcommand handler)
- `test_init_writes_config_and_keys`
- `test_status_against_mock_node_returns_table`
- `test_call_streams_stdout_then_zero`
- `test_doctor_exit_code_reflects_failures`
- `test_erase_keep_keys`
### Integration
- `test_full_init_then_run_then_status` β spawn subprocess, await readiness, query
- `test_call_returns_nonzero_on_wire_error`
- `test_export_zip_is_signed_and_parseable`
---
## 9. Cross-references
| What | Where |
|------|-------|
| Self-diagnostics | [X03 Β§6](../cross-cutting/X03-observability.md) |
| Onboarding helpers | [M13](M13-onboarding.md) |
| Bus introspection endpoints | [M03 Β§3.7](M03-bus.md), [X01 Β§3.2](../cross-cutting/X01-transport.md) |
| Trace ring buffer endpoint | [X01 Β§3.2](../cross-cutting/X01-transport.md), [X03 Β§5](../cross-cutting/X03-observability.md) |
| Config | [X04](../cross-cutting/X04-config.md) |
---
## 10. Open questions
1. **Daemon mode on Linux** β `systemd` user unit? Ship one in packaging? Phase 1.5.
2. **Windows service / macOS LaunchAgent** β Phase 2.
3. **Shell completion** β Click supports it; ship completions for bash/zsh/fish.
4. **Progress bars for ingest / fetch** β `rich` progress; nice but optional.
|