Plan and working notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
commit
a6ece28344
3 files changed
+505
No files matched your search
@@ -0,0 +1,430 @@
|
||||
# ai-app — plan
|
||||
|
||||
A phone interface to AI coding sessions — Claude Code and llama.cpp for now —
|
||||
built to replace the Claude app for day-to-day use. Two motivations: local
|
||||
models need a front end at all, and owning the client means fixing the things
|
||||
the official app gets wrong (e.g. it won't deliver a typed message until the
|
||||
session fully finishes its turn, where the TUI injects it at the next tool
|
||||
boundary).
|
||||
|
||||
Same shape as `../local-updater`: a Rust (Axum) backend on the desktop, a
|
||||
Kotlin/Compose Android app, pinned self-signed TLS between them.
|
||||
|
||||
## The one idea everything hangs off
|
||||
|
||||
Both session types are **a child process speaking JSONL over stdio**:
|
||||
|
||||
- Claude Code: `claude -p --input-format stream-json --output-format stream-json`
|
||||
— bidirectional streaming JSON. User messages sent while a turn is running
|
||||
are injected at the next opportunity (the TUI behavior we want), a control
|
||||
protocol carries interrupts and permission requests, `--resume <id>` picks a
|
||||
session back up after a backend restart.
|
||||
- llama.cpp: **pi in RPC mode** (`pi --mode rpc`), pointed at a llama-server
|
||||
endpoint. Same deal: JSONL on stdio, `prompt` (with images), `steer` for
|
||||
mid-run injection, `abort`, `set_model`, `compact` / `set_auto_compaction`,
|
||||
session files that survive restarts, structured events for streaming text
|
||||
and tool executions.
|
||||
|
||||
So the backend has one abstraction — spawn a process, translate its dialect to
|
||||
a common event stream, keep an append-only transcript — and two translators.
|
||||
SSH support falls out of the same shape: a remote session is the identical
|
||||
command run as `ssh <host> <cmd>`; stdio doesn't care.
|
||||
|
||||
Decisions already made (2026-08-24):
|
||||
|
||||
- llama.cpp harness: **pi RPC now**, with the session abstraction kept clean
|
||||
enough that a custom Rust agent loop can be added as a third driver later.
|
||||
- The backend **manages llama-server itself** (start with a chosen GGUF, stop,
|
||||
swap models), locally and over SSH.
|
||||
- Claude permission prompts are **interactive in the app**, with a per-session
|
||||
permission mode chosen at spawn.
|
||||
- **One backend** on the main machine; the phone talks only to it, and it
|
||||
reaches other hosts via SSH. Remote hosts need the CLIs installed but no
|
||||
backend.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Android app (Compose)
|
||||
│ HTTPS (pinned CA) — REST for actions, SSE for live events
|
||||
▼
|
||||
backend (Rust/Axum, desktop)
|
||||
├─ SessionManager ── Session ── Driver (trait)
|
||||
│ ├─ ClaudeDriver (claude stream-json)
|
||||
│ └─ PiDriver (pi --mode rpc)
|
||||
│ each driver's process is spawned locally or as `ssh host …`
|
||||
├─ LlamaServerManager (llama-server lifecycle, local + SSH)
|
||||
├─ UsageMonitor (Anthropic OAuth usage endpoint)
|
||||
└─ config.json + per-session transcript files
|
||||
```
|
||||
|
||||
### Backend layout (`server/`)
|
||||
|
||||
Mirroring local-updater's stack: axum 0.8, axum-server + rustls, tokio, serde,
|
||||
clap, tracing. Rust edition 2024, warning-clean, clippy in CI habit.
|
||||
|
||||
- `main.rs` — bootstrap, TLS listener.
|
||||
- `routes.rs` — the whole HTTP table in one module doc comment (as in
|
||||
local-updater).
|
||||
- `session/mod.rs` — `SessionManager`: the live session registry, every
|
||||
mutation funnels through it (the `registry.rs` pattern: in-memory and
|
||||
on-disk state can't come apart).
|
||||
- `session/driver.rs` — the `Driver` trait and the common event model.
|
||||
- `session/claude.rs`, `session/pi.rs` — the two translators.
|
||||
- `session/transcript.rs` — append-only JSONL event log per session, with
|
||||
monotonically increasing sequence numbers (the phone's resume cursor).
|
||||
- `llama.rs` — `LlamaServerManager`.
|
||||
- `hosts.rs` — host configs and the ssh command builder.
|
||||
- `usage.rs` — Anthropic usage polling.
|
||||
- `config.rs` — persisted schema.
|
||||
|
||||
### The common event model
|
||||
|
||||
Driver output, whatever the dialect, is normalized into one event enum before
|
||||
it touches the transcript or the phone:
|
||||
|
||||
- `AssistantText { delta }` — streaming text (rendered as markdown).
|
||||
- `ToolStart / ToolUpdate / ToolEnd { tool, input, output }` — the "view tools
|
||||
it's running" screen is just these.
|
||||
- `Image { ref }` — images in output (screenshots from tools, etc.) are saved
|
||||
under the session dir and referenced by id; the phone fetches them by URL.
|
||||
- `Question { id, prompt, options }` — anything the session needs a human for:
|
||||
Claude's AskUserQuestion, and **permission requests** (canUseTool) are the
|
||||
same shape with approve/deny options. Answered via one endpoint.
|
||||
- `Status { state }` — idle / running / awaiting-input / compacting / exited.
|
||||
- `UsageDelta { tokens }` — per-turn token counts where the dialect reports
|
||||
them (both do).
|
||||
- `Error { message }`.
|
||||
|
||||
Every event is appended to the session's transcript file with a sequence
|
||||
number, then fanned out to any connected SSE subscribers. The phone renders
|
||||
purely from this stream: reconnecting means "give me events after seq N" —
|
||||
no separate "load history" path to drift from the live one.
|
||||
|
||||
Inbound, the driver trait is small:
|
||||
|
||||
```rust
|
||||
trait Driver {
|
||||
fn send_user_message(&self, text: String, images: Vec<ImageRef>);
|
||||
fn answer_question(&self, id: QuestionId, answer: Answer);
|
||||
fn interrupt(&self); // stop mid-run, session survives
|
||||
fn set_model(&self, model: &str);
|
||||
fn compact(&self); // pi: native; claude: /compact
|
||||
fn shutdown(&self); // graceful process exit
|
||||
}
|
||||
```
|
||||
|
||||
`send_user_message` during a run is the point of the whole app: both dialects
|
||||
queue it for injection at the next tool boundary rather than the end of the
|
||||
turn. Claude's dialect: a `user` message on stdin mid-stream; pi's: `steer`.
|
||||
|
||||
### Claude driver specifics
|
||||
|
||||
- Spawn: `claude -p --verbose --input-format stream-json --output-format
|
||||
stream-json --permission-mode <mode>` in the chosen working directory, plus
|
||||
`--model` at spawn. Permission mode (default/plan/acceptEdits/
|
||||
bypassPermissions) is chosen on the spawn screen.
|
||||
- Interactive permissions: run with the stream-json control protocol's
|
||||
permission request flow (the same mechanism the Agent SDK's `canUseTool`
|
||||
uses) so tool approvals arrive as control requests, become `Question`
|
||||
events, and our answer goes back as the control response. **Verify the
|
||||
exact control-request wire format against the current CLI early in
|
||||
implementation** — it's the least-documented part of this plan.
|
||||
- Interrupt: control-protocol interrupt request.
|
||||
- Model change mid-session: try the control protocol's set-model; if the
|
||||
installed CLI doesn't support it, fall back to `shutdown` + respawn with
|
||||
`--resume <session_id> --model <new>` — cheap, since Claude persists
|
||||
sessions in `~/.claude/projects` anyway. That same resume path is the crash
|
||||
recovery story: a dead backend or a killed process loses nothing.
|
||||
- Images in: base64 image content blocks in the stream-json user message.
|
||||
- Working directory, host, and model are spawn-screen fields.
|
||||
|
||||
### pi driver specifics
|
||||
|
||||
- Spawn: `pi --mode rpc --provider openai-generic --model <name>` (endpoint =
|
||||
the llama-server the LlamaServerManager provides), `--session-dir` under
|
||||
our session storage so transcripts and pi's own session files live together.
|
||||
- Auto-compaction on by default (`set_auto_compaction`), threshold
|
||||
configurable per session; manual `compact` exposed as a button.
|
||||
- `steer` for mid-run messages, `abort` for stop, `set_model` when the target
|
||||
endpoint changes.
|
||||
- pi's session JSONL gives resume-after-restart, same as Claude's.
|
||||
|
||||
### llama-server management
|
||||
|
||||
`config.json` lists **models** (name → GGUF path or llama-server args, per
|
||||
host) and **hosts**. The manager runs at most one llama-server per
|
||||
`(host, model)`, spawned on demand when a session needs it:
|
||||
|
||||
- Spawn (local or `ssh host llama-server …`) on an allocated port, wait on
|
||||
`/health`, hand the endpoint to the pi driver.
|
||||
- Refcounted by sessions. The path out, written in the same change as the
|
||||
spawn: the last session using an instance releasing it starts an idle
|
||||
timer (configurable, e.g. 10 min), after which it's killed. Delete of the
|
||||
last session kills it immediately.
|
||||
- "Change model" on a llama session = acquire the new model's server,
|
||||
`set_model` on pi, release the old one. Context carries over (it's
|
||||
prompt-replayed by pi against the new endpoint).
|
||||
- Remote llama-server output is only reachable from the backend host, and
|
||||
binds localhost on the remote side with an SSH local port forward
|
||||
(`ssh -L`) held by the manager — no LAN-exposed inference ports.
|
||||
|
||||
### SSH
|
||||
|
||||
- Host entries in `config.json`: name, `user@host`, optional ssh options,
|
||||
which capabilities it has (claude / pi / llama-server, with paths if not on
|
||||
PATH). Key-based auth only, using the system `ssh` client via
|
||||
`tokio::process` — no Rust SSH library; this inherits `~/.ssh/config`,
|
||||
agents, and jump hosts for free. (Rule 23: openssh is already here and
|
||||
battle-tested; a library buys nothing but a second config surface.)
|
||||
- A remote session is exactly a local one with the command wrapped in
|
||||
`ssh -T host …`. Process death ≙ connection death; the session shows as
|
||||
`exited` and both dialects resume (`--resume` / pi session file) on respawn,
|
||||
so a dropped SSH connection is an annoyance, not data loss.
|
||||
- Images and attachments for remote sessions are written to the remote
|
||||
session dir via `scp`/stdin before the message referencing them is sent.
|
||||
|
||||
### Usage limits (Claude)
|
||||
|
||||
Poll `https://api.anthropic.com/api/oauth/usage` — the same endpoint behind
|
||||
Claude Code's `/usage` — with the OAuth access token from Claude Code's local
|
||||
credential store (`~/.claude/.credentials.json`), headers
|
||||
`anthropic-beta: oauth-2025-04-20` and `User-Agent: claude-code/<version>`
|
||||
(without the User-Agent it lands in an aggressively rate-limited bucket).
|
||||
Poll at ≥180 s, only while any Claude session exists or the usage screen is
|
||||
open, cache the last answer. Surface: 5-hour and weekly window utilization %
|
||||
and reset times. It's undocumented, so `usage.rs` treats every field as
|
||||
optional and degrades to "unavailable" rather than erroring. Structure it as
|
||||
one `UsageProvider` per paid service so a second service later is a new impl,
|
||||
not a parallel screen (rule 9).
|
||||
|
||||
### HTTP surface (phone ⇄ backend)
|
||||
|
||||
REST for actions, one SSE stream per open session screen for events, all over
|
||||
the pinned TLS listener. SSE over WebSocket because resume-by-cursor
|
||||
(`Last-Event-ID` = transcript seq) is native to it and the inbound direction
|
||||
is plain POSTs anyway.
|
||||
|
||||
```
|
||||
GET /sessions list (id, kind, title, host, model, status, last activity)
|
||||
POST /sessions spawn {kind, host, model, cwd, permission_mode, title}
|
||||
GET /sessions/:id/events?after=N SSE: transcript replay from N, then live
|
||||
POST /sessions/:id/message {text, attachment_ids}
|
||||
POST /sessions/:id/answer {question_id, answer} (questions and permissions)
|
||||
POST /sessions/:id/interrupt
|
||||
POST /sessions/:id/model {model}
|
||||
POST /sessions/:id/compact (llama sessions)
|
||||
DELETE /sessions/:id kill process, release llama-server, delete transcript+files
|
||||
POST /attachments multipart upload → id (referenced by /message)
|
||||
GET /files/:session/:id images/files the session produced
|
||||
GET /usage cached usage windows
|
||||
GET/PUT /hosts, /models config editing from the phone
|
||||
```
|
||||
|
||||
Sessions live in `config.json` + a per-session directory (transcript.jsonl,
|
||||
attachments, produced images). Deleting a session is the complete path out of
|
||||
everything spawning one created.
|
||||
|
||||
### Security
|
||||
|
||||
- TLS with a self-signed CA, pinned in the app — `gen-dev-cert.sh` and
|
||||
`PinnedCert.kt` copied from local-updater, same idempotent-CA/reissued-leaf
|
||||
scheme, same one-way-door caveat about regenerating the CA.
|
||||
- This server is strictly more dangerous than the updater: its API *is*
|
||||
remote code execution (spawn a bypass-permissions Claude on any SSH host).
|
||||
Pinning authenticates the server to the phone but not the phone to the
|
||||
server, so a bearer token adds the other direction. Threat model: the token
|
||||
gates LAN-reachable RCE; it does not (and cannot) defend a compromised
|
||||
backend host or phone — those are inside the trust boundary, and a
|
||||
compromised phone is handled by rotation.
|
||||
- **Generation**: 256 bits from the OS CSPRNG on first run, base64url. A
|
||||
machine credential, never typed twice, so unguessable costs nothing; at
|
||||
this entropy no key stretching is needed.
|
||||
- **Enrollment**: printed once as a terminal QR code (`qrcode` crate,
|
||||
ANSI), encoding `aiapp://enroll?host=…&port=…&token=…`. The CA stays
|
||||
embedded in the APK (`PinnedCert.kt` pattern), so the QR carries no
|
||||
trust material — photographing the terminal leaks only the token
|
||||
(rotatable), never a way to weaken pinning.
|
||||
- **Storage**: server keeps only the SHA-256 in `config.json` (plain hash
|
||||
is enough for high-entropy random input; buys that a leaked config
|
||||
doesn't leak the credential). No "show token again" — lost means rotate.
|
||||
Phone side: Keystore-backed encrypted preferences.
|
||||
- **Transport**: `Authorization: Bearer` header on every request including
|
||||
the SSE GET. Never a query parameter (URLs leak into logs). The tracing
|
||||
layer must not log the header — covered by a test so a logging change
|
||||
can't silently start leaking it.
|
||||
- **Verification**: one middleware wrapping the entire router in `main.rs`,
|
||||
never per-route, so a new route can't forget auth. Zero unauthenticated
|
||||
endpoints, `/health` included. Hash-then-constant-time-compare
|
||||
(`subtle`); failures logged with peer address plus a small fixed delay —
|
||||
not against brute force (infeasible at 256 bits) but so scanners show up
|
||||
in the log.
|
||||
- **Rotation (the path out)**: `--rotate-token` regenerates, invalidates
|
||||
the old hash immediately, reprints the QR. That's the whole lost-phone
|
||||
story. Config stores a *list* of `{name, hash}` (of one, today) so
|
||||
per-device tokens with individual revocation are a config entry later,
|
||||
not a schema migration.
|
||||
- **Why not mTLS**: stronger in theory (key never leaves the Keystore, no
|
||||
bearer secret to exfiltrate), but given pinning the delta is only
|
||||
"someone reads the token off a device already inside the trust
|
||||
boundary", and it costs Android client-cert provisioning ceremony and a
|
||||
worse new-phone story than a QR scan. Revisit if this outgrows
|
||||
single-user-on-LAN.
|
||||
- **Off-network access: plain WireGuard** (decided 2026-08-24; no third
|
||||
party). The backend binds to the WireGuard interface (`wg0`) only; the
|
||||
phone runs the official WireGuard app (always-on VPN, per-app tunneling),
|
||||
enrolled by scanning its config as a terminal QR
|
||||
(`qrencode -t ansiutf8 < phone.conf` — same gesture as token enrollment).
|
||||
The only internet-visible thing is one forwarded UDP port that is silent
|
||||
to unauthenticated packets — scanners see it as closed — so the app's
|
||||
pre-auth surface (rustls handshake, hyper parsing, auth middleware) is
|
||||
reachable only from enrolled peers, and the token becomes defense in depth
|
||||
rather than the sole gate. Addressing stays single-path: the phone reaches
|
||||
the backend at its WireGuard address (e.g. `10.66.0.1`) from everywhere —
|
||||
one address in the app, one SAN in the leaf cert (`SERVER_IP=`/SAN
|
||||
override in the cert script), no home/away distinction. Another machine
|
||||
later is one keypair + one `[Peer]` block.
|
||||
- Operational needs, accepted: a public endpoint hostname. The home IP is
|
||||
mostly static but not guaranteed, so the phone's endpoint is a DDNS name
|
||||
(free, e.g. DuckDNS, or the router's built-in client; a curl cron on the
|
||||
backend host works too) that tracks changes automatically. One WireGuard
|
||||
nuance: the phone app resolves the endpoint hostname when the tunnel
|
||||
comes up and does not re-resolve on its own, so on the rare IP change
|
||||
the fix is toggling the tunnel off/on once DDNS has caught up (minutes).
|
||||
The symptom is obvious (app can't reach the backend) and lossless — the
|
||||
SSE cursor design means reconnects replay whatever was missed. Also:
|
||||
at-home traffic rides NAT hairpinning on the router (verify early; most
|
||||
support it, and the fallback is toggling the tunnel off at home).
|
||||
- Rejected: **Tailscale** — same WireGuard underneath with easier setup
|
||||
(no port forward, LAN peer discovery), but it adds a third-party
|
||||
coordination service and account this setup doesn't need at two or
|
||||
three devices; **Headscale** — self-hosting that coordination server is
|
||||
strictly more moving parts than one wg config per peer at this scale;
|
||||
**forwarding the HTTPS port directly** — puts every internet scanner
|
||||
one pre-auth bug away from RCE on a machine holding SSH keys.
|
||||
- The server still refuses to start without TLS — no plaintext listener
|
||||
exists even inside the tunnel, so the token can't travel unencrypted by
|
||||
misconfiguration, and interface binding failing closed (refuse to start
|
||||
if `wg0` is absent, rather than falling back to 0.0.0.0) is part of the
|
||||
same guarantee.
|
||||
- The bootstrap-over-HTTP trick from the updater is unnecessary here — the
|
||||
app installs via Local Updater.
|
||||
|
||||
## App (`app/`)
|
||||
|
||||
Kotlin + Compose Multiplatform, single `:androidApp` module, same versions as
|
||||
local-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
|
||||
|
||||
1. **Session list** — cards: kind icon, title, host, model, status
|
||||
(running / awaiting answer / idle / exited), last activity. Spawn FAB;
|
||||
swipe/long-press to delete (confirm). Sessions awaiting an answer sort to
|
||||
the top — that's the "your turn" inbox.
|
||||
2. **Spawn** — kind, host (from config), model (Claude list is static+editable;
|
||||
llama list from config), working directory, permission mode (Claude),
|
||||
title.
|
||||
3. **Session screen** — the core:
|
||||
- Transcript rendered from the event stream: markdown text, inline images,
|
||||
collapsed-by-default tool cards (name + input summary, expandable to
|
||||
output; a spinner while `ToolStart` has no matching `ToolEnd`).
|
||||
- Question cards inline: option buttons for AskUserQuestion, allow/deny for
|
||||
permissions, free-text where allowed.
|
||||
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
||||
mid-run sends become steering messages.
|
||||
- Top bar: model chip (tap to change), stop button while running, token
|
||||
count, compact button (llama), overflow → delete.
|
||||
4. **Usage** — window bars for the 5-hour and weekly limits with reset times.
|
||||
5. **Settings** — server address + token, hosts editor, llama model list
|
||||
editor.
|
||||
|
||||
Networking mirrors local-updater's app layer (`AppsApi.kt` style thin client +
|
||||
pinned transport), plus an SSE client with `after=` resume driven by
|
||||
connectivity/lifecycle. The app keeps no persistent transcript store — the
|
||||
backend's transcript is the source of truth; the app caches only for the
|
||||
screen it's showing.
|
||||
|
||||
## Compaction: options explored
|
||||
|
||||
Context: raw llama-server has no conversation memory management; the context
|
||||
window just fills.
|
||||
|
||||
1. **pi's auto-compaction** — *chosen*. When the prompt nears the model's
|
||||
context limit, pi summarizes older history with the model itself and
|
||||
replaces it with a structured summary; threshold configurable; manual
|
||||
`compact` also exposed. Battle-tested, zero work for us.
|
||||
2. **Manual compaction in a custom Rust loop** — *the later third driver*.
|
||||
The design when we build it: every llama-server response reports prompt +
|
||||
completion token counts; track them against `n_ctx` (from `/props`); at a
|
||||
threshold (~75%), pause, run a summarization request over all but the last
|
||||
few turns ("state of the task, decisions made, open items, relevant
|
||||
file/tool state"), replace those turns with the summary as a system-adjacent
|
||||
message, continue. Keep the full pre-compaction transcript on disk — the
|
||||
phone view never loses history, only the model's view shrinks. Worth doing
|
||||
eventually for control over the summarization prompt and for tool-loop
|
||||
experiments pi doesn't allow.
|
||||
3. **llama-server `--context-shift`** — *rejected* as the strategy. It
|
||||
truncates old KV cache entries: silent forgetting, no summary, and it
|
||||
corrupts the harness's view of what the model knows. Fine as a server-side
|
||||
safety net; not memory management.
|
||||
|
||||
## Phases
|
||||
|
||||
1. **Skeleton** — repo layout, cert script, TLS + token auth, wg0-bound
|
||||
listener (fail closed if the interface is missing), config.json,
|
||||
session registry with a fake `EchoDriver`, session list + session screen in
|
||||
the app end-to-end over SSE. Proves the whole pipe before any AI is
|
||||
involved.
|
||||
2. **Claude local** — ClaudeDriver: spawn, stream text/tools, mid-run send,
|
||||
interrupt, permission questions, AskUserQuestion, images both ways, delete.
|
||||
*Milestone: daily-drivable Claude replacement on localhost.*
|
||||
3. **Usage screen.**
|
||||
4. **llama.cpp** — LlamaServerManager (local), PiDriver, model change,
|
||||
compaction controls.
|
||||
5. **SSH** — host config, remote spawn for both kinds, remote llama-server
|
||||
with port forward, attachment shipping.
|
||||
6. **Polish** — reconnect edges, notification when a session awaits an answer
|
||||
(the "your turn" push), transcript search, whatever daily use surfaces.
|
||||
|
||||
Each phase ends runnable and verified against the real thing (rule 22); the
|
||||
backend gets tests where logic is pure (event normalization, transcript
|
||||
cursors, config persistence, refcounting) — the app is UI over the API and is
|
||||
verified by running it, matching local-updater's posture.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
- **Claude stream-json control protocol details** (permission requests,
|
||||
set-model, interrupt wire format) are the least-documented dependency and
|
||||
version-coupled to the installed CLI. Phase 2 starts by probing the
|
||||
installed version and pinning what works; the `--resume` respawn fallback
|
||||
covers whatever the control channel can't do.
|
||||
- The **usage endpoint is undocumented** and has changed rate-limit behavior
|
||||
before; treat as best-effort.
|
||||
- **pi RPC schema drift** — pin a pi version; the translator is one file.
|
||||
- Whether **notifications** need FCM or a foreground-service polling
|
||||
connection — decide in phase 6; the SSE cursor design already supports
|
||||
either.
|
||||
- Claude sessions over SSH need the remote host **logged in to Claude**; usage
|
||||
reporting reads only the backend host's credentials. Acceptable for now
|
||||
(same account everywhere); revisit if not.
|
||||
|
||||
## References
|
||||
|
||||
Research behind the decisions above (verified 2026-08-24; re-check against
|
||||
installed versions when each phase starts):
|
||||
|
||||
- pi RPC protocol: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/rpc.md
|
||||
— commands (`prompt`, `steer`, `follow_up`, `abort`, `set_model`,
|
||||
`compact`, `set_auto_compaction`, session ops) and the event stream.
|
||||
- pi + llama-server in practice: https://medium.com/@tolgaeren/running-pi-with-local-llms-c596aa14b062
|
||||
- llama-server API (`/health`, `/props`, OpenAI-compatible endpoints,
|
||||
`--context-shift`): https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
|
||||
and the offline-agentic-coding walkthrough:
|
||||
https://github.com/ggml-org/llama.cpp/discussions/14758
|
||||
- Usage endpoint (`GET https://api.anthropic.com/api/oauth/usage`, bearer
|
||||
token from `~/.claude/.credentials.json`, headers
|
||||
`anthropic-beta: oauth-2025-04-20` + `User-Agent: claude-code/<version>`,
|
||||
≥180 s polling; wrong User-Agent → aggressive 429 bucket):
|
||||
https://github.com/anthropics/claude-code/issues/31637 and
|
||||
https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/202
|
||||
- Sibling project this repo's conventions mirror: `../local-updater`
|
||||
(README.md + AGENTS.md — server/registry/routes layout, cert scheme,
|
||||
testing posture, Android env notes).
|
||||
Reference in new issue
Block a user