Seven loose root-level .ts files were the dark-mechanicus theme bundle:
indicator, banner, footer, status-line, session-names, thinking-label,
and markdown-body-color. They share visual language with
themes/dark-mechanicus.json and only make sense together.
Moved them under dark-mechanicus/ as a multi-file extension. The
pi-coding-agent loader auto-discovers <subdir>/index.ts as a single
extension entry, so dark-mechanicus/index.ts now sequences each
sub-module's default registrar.
File renames also drop the redundant 'mechanicus-' / 'dark-mechanicus-'
prefix since the directory name carries it. Symbol names, command names,
and external behavior are unchanged — disabling each piece still works
via its own /<name>-off command.
Imports inside each moved file: ./shared/ → ../shared/.
local-llama.ts stays at the root — separate stub provider, not part of
the theme.
.gitignore updated: 8 individual !/<file>.ts allowlist entries replaced
with a single !/dark-mechanicus/.
README.md: theme entries collapsed into one row pointing at the folder;
tree diagram updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minimal shell wrapper around llama.cpp router's OpenAI-compatible API
(/v1/chat/completions), gated by the same mTLS cert as the pi extension.
Single-file, runtime deps: bash + curl + jq. Useful for scripts and agents
(Claude Code, etc.) that want to delegate generation without pulling in
a full SDK.
Features:
--list / --status / --load <model>
--stream <model> "..." for SSE token-stream output
--raw <model> '...' for full openai-format json bodies (also @file)
--prompt-file <path> reads prompt from disk via jq --rawfile, bypassing
Linux's MAX_ARG_STRLEN (~128KB per argv) so prompts
up to the model's context window work
--temperature / --top-p / --max-tokens / --system sampling overrides
Auto-retry with exponential backoff on transient empty/non-JSON
responses (model-loading window). Short-circuits on structured 4xx
errors (e.g. exceed_context_size).
AI_CERT_DIR / AI_ENDPOINT / AI_RETRIES env overrides.
Includes scripts/AI-COMPLETE.md with install + usage docs and a row in
the top-level README's scripts table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typo in the issuer org for newly minted client certs. Existing certs are
unaffected (Caddy validates against the root CA's public key, not subject
text). Future certs issued via this script will carry the corrected
O=Shahondin1624.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit referenced shared/ansi.ts, shared/format.ts, and
shared/ctx.ts but those files were filtered by the default-deny
.gitignore. Adding !/shared/ to the allowlist so the imports actually
resolve in a fresh clone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit produced five concrete improvements:
1) New shared/ module (zero-dep pure utilities)
- shared/ansi.ts: hexToRgb (throws on malformed input instead of
silently producing NaN), fgFromHex, stripAnsi, visibleWidth,
ANSI_RESET_FG / ANSI_RESET_ALL constants.
- shared/format.ts: formatTokens, formatElapsed.
- shared/ctx.ts: safely() and safelyAsync() helpers for dealing with
pi's "stale after session replacement or reload" ExtensionRunner
semantics.
Removes duplicate helpers from mechanicus-footer, markdown-body-color,
dark-mechanicus-indicator.
2) ai-server: non-blocking startup + short-race timeout
- Factory registers STATIC_MODELS immediately so pi startup isn't
blocked on the HTTPS round-trip.
- Races discoverModels() against a 300ms timeout. On LAN (~40ms) the
live list wins and pi --list-models sees the real models. Past the
timeout, fallback remains and background discovery updates the
provider later.
- listModelsCached() with 5s TTL for tab completions (was firing a
round-trip on every keystroke).
- loadModel/unloadModel invalidate the cache.
3) dark-mechanicus-indicator: stale-ctx guard
- Wrap the setInterval ticker body in safely() so a race between
session_shutdown and the ticker can't crash node. Same pattern as
the earlier footer fix.
4) Safer monkey-patches in markdown-body-color and mechanicus-thinking-label
- Feature-detect Markdown/Editor/AssistantMessageComponent's target
method before patching. Warn-and-skip rather than silently create
a broken prototype if a pi-tui upgrade renames the internal method.
5) Minor
- Replaced five `as any` casts with typed Record<string, unknown>
access in the monkey-patch sites.
- ai-server debug log only fires when actual discovery succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On /quit (and /reload), pi marks the ExtensionRunner stale immediately,
but the TUI can still fire a pending render timer before tearing down.
Our footer's render() then accessed cachedCtx.sessionManager, which
throws "This extension instance is stale after session replacement or
reload" — uncaught, crashing Node with a stack trace on the way out.
Two layers of defense:
1. pi.on("session_shutdown", () => { cachedCtx = null; })
- Runs at shutdown, before the final render timer fires in most
cases, so render() sees null ctx and returns [] cleanly.
2. try/catch around render body
- For the tight race where a render fires between the stale-flag
being set and our session_shutdown handler running, swallow the
error and return []. No more stack traces on quit.
Extracted the render body into a `renderInner` helper for readability
during the try/catch wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier I removed the `pi.sendMessage` call that produced
"Cogitated for Xs" custom messages, along with their renderer and
context filter. But sessions that were alive when the feature existed
retained those custom-message entries in their persistent log —
pi.sendMessage writes CustomMessageEntry records to the session file,
and removing the extension does not retroactively delete them.
Without the context filter, those entries still flow through
convertToLlm() and become role:"user" messages in every provider
request. The model started responding to them ("the user is asking...
The 'Cogitated' messages suggest they're reading/thinking...").
Re-adding the `context` event filter that skips any custom message
with customType "cogitation-summary". No-op for sessions without those
entries; retroactive cleanup for sessions that have them. Does not
bring back the feature itself — no sendMessage or renderer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pi's session can contain assistant entries whose AssistantMessage.content
is entirely thinking blocks (no text, no tool calls) — typical after an
aborted turn or when reasoning is edited out. Our contextToOpenAIMessages
was emitting those as { role: "assistant", content: null }.
When such a message is at the end of the context, llama.cpp's chat
template interprets the trailing assistant entry as an "assistant
response prefill" attempt. Reasoning-model templates (MiniMax M2.7,
Qwen, etc.) have enable_thinking set, and the server rejects this
combination with HTTP 400:
"Assistant response prefill is incompatible with enable_thinking."
Fix: skip assistant entries where extractAssistantText and
extractToolCalls both return empty. Thinking blocks aren't re-fed to
the model anyway, so dropping the wrapper message loses no information.
+ two regression tests in tests/messages.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists every extension, the theme, setup scripts, tests, a quick
onboarding section, and a repo layout diagram. Links each extension
to its source file. Specifically notes the deferred stream-parsing
tests and the reason (mock HTTPS harness not worth it for single-user
deployment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor:
- Extracted extractCtxSize + isShardArtefact from ai-server/admin.ts to
a new ai-server/router-utils.ts with zero relative imports. Makes them
directly loadable in tests with Node's --experimental-strip-types
(no jiti needed). admin.ts re-exports extractCtxSize so index.ts is
unchanged.
tests/router-utils.test.ts (9 cases):
- extractCtxSize: present/value, missing, end-of-argv, non-numeric,
zero (edge), missing status.
- isShardArtefact: positive cases (5-digit, numeric, no zero-padding),
negative cases (clean preset names, non-shard numeric patterns,
shard pattern mid-string).
tests/integration.test.ts (2 new cases):
- "server cert is publicly trusted": verifies curl without --cacert
flag reaches /health. Catches LE regression (cert reverting to
self-signed).
- "chat completion returns usage with prompt_tokens_details":
sanity-checks the server contract our stream.ts now reads for
cache-token reporting. Picks any loaded runnable model; skips
cleanly when none loaded.
Totals: 28 tests (13 messages + 9 router-utils + 6 integration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caused more trouble than it was worth — even with the context-event
filter stripping the custom message from LLM input, the transcript
entry could still influence the agent in subtle ways (model looking
at it on re-render, extension-interactions, export), and the visual
clutter after every turn wasn't worth the diagnostic value.
Removed:
- pi.registerMessageRenderer("cogitation-summary", ...)
- pi.sendMessage on turn_end
- pi.on("context") filter (no longer needed; nothing to strip)
- Box/Text imports from @mariozechner/pi-tui
- SUMMARY_TYPE constant
Kept:
- Live indicator: ⚙ <quote> · <elapsed> during streaming, with the
cog pulse and quote rotation and the "Working..." suppression.
- Footer tok/s (lives in mechanicus-footer, not affected).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause for leak: pi's convertToLlm() turns every custom message
into a role:"user" entry with the message's content text before sending
to the provider. Our "Cogitated for 1m 24s" custom message was being
fed to the model on the next turn, and the model was responding to it
as if it were a user prompt.
Fix: dark-mechanicus-indicator now registers a `context` event handler
that returns a filtered messages array with `cogitation-summary`
entries stripped. The summary still renders inline for the user
(display:true), but the LLM never sees it.
mechanicus-footer additions:
- Track turn_start/turn_end to capture the most recent turn's wall
duration (lastTurnDurationMs).
- Compute generation speed as lastAssistantOutput / lastTurnDuration
and surface it in the stats line as "12.3tok/s" (shown only when a
turn has completed and we have real output tokens to report).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces pi's default footer via ctx.ui.setFooter with a near-identical
layout but two key changes:
- Context usage: "45k/131k (34%)" instead of pi's default "34%/131k".
You see the absolute current token count next to the max, with the
percentage in parens — no more mental math to know how many tokens are
actually in play.
- Compaction counter: appends "C<N>" when the session has been compacted
(reads CompactionEntry count from sessionManager.getEntries()). Hidden
when 0.
- Preserves: ↑in ↓out R<cache> $cost · ctx · C<N> · model • thinking level,
plus extension statuses on line 3+.
- Preserves context-percentage color thresholds (>90% error, >70% warning).
Commands: /mechanicus-footer-off (restore default), /mechanicus-footer-on
(reinstall).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small fixes:
ai-server/stream.ts
- llama.cpp reports cached prompt tokens via
usage.prompt_tokens_details.cached_tokens
and we were ignoring it. Populate output.usage.cacheRead so pi's
footer can show the "R<tokens>" field. cacheRead is a subset of
prompt_tokens (already counted in input), so totalTokens stays
input + output — no double-counting.
dark-mechanicus-indicator.ts
- Pi appends "Working... (ESC to interrupt)" next to custom working
indicator frames via a separate message slot. Call
ctx.ui.setWorkingMessage("") on session_start + every turn_start to
clear that suffix so the indicator line is just
⚙ <quote> · <elapsed>
with no trailing "Working...".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously there were two "work indicators" running in parallel:
1. The pulsing-cog working indicator (⚙ + rotating quote)
2. A separate footer timer ("⚙ Cogitating... (Xm Ys)")
Merges them:
- During generation: the working indicator now shows
"⚙ <quote> · 1m 24s"
on one line. A 1s setInterval rebuilds 4 frames (one full pulse cycle)
with the current quote + current elapsed time; the reset lands at
frame 0 of the pulse so the animation looks seamless.
- After generation: a new inline message appears in the transcript right
after the last assistant message:
"✴ Cogitated for 1m 57s"
Rendered via pi.registerMessageRenderer + pi.sendMessage with a
"cogitation-summary" customType.
Removes mechanicus-cogitation-timer.ts (the old footer timer). The
mechanicus-status-line extension (HERETEK FORGE · ACTIVE flavor line)
stays — it's a separate status, not a work indicator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shows elapsed-time counters in the footer during generation and a final
"Cogitated for Xm Ys" summary after the turn ends.
- turn_start: seed "⚙ Cogitating... (0s)" and start a 1s setInterval
that refreshes the footer with the current elapsed time.
- turn_end: clear the ticker, swap to "✴ Cogitated for Xm Ys", and
schedule a cleanup after 30s so the footer isn't permanently cluttered.
- session_shutdown: safety net to prevent zombie timers on reload.
Uses ctx.ui.setStatus so this runs alongside the existing pulsing-cog
working-indicator without interfering with its animation pointer (which
would reset on every setWorkingIndicator call).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that Caddy serves a Let's Encrypt cert for ai.shahondin1624.de,
passing `ca: root-ca.pem` to https.request made Node override its
default trust list with only the private CA — which no longer chains
the LE-issued server cert, so every request failed with
ECONNRESET / CERT_HAS_EXPIRED-style errors on the client side.
Dropping the `ca:` option lets Node fall back to its built-in Mozilla
CA bundle, which includes Let's Encrypt. Client cert/key still passed;
mTLS remains enforced server-side.
If the server ever reverts to a self-signed cert, re-add `ca: certs.ca`
or set NODE_EXTRA_CA_CERTS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dark-mechanicum status line to pi's footer via ctx.ui.setStatus.
The default footer automatically renders registered extension statuses
on a third line below cwd + stats.
- 15 status strings (HERETEK FORGE · ACTIVE, VASHTORR NETWORK · ONLINE,
etc.), pseudo-randomly picked.
- Seeded on session_start, rotated on every turn_end. Status persists
between turns so it doesn't flicker during input.
- Coloured: ⚙ prefix in warning/gold, body in muted/bronze.
- Commands: /mechanicus-status-cycle (force new), /mechanicus-status-off.
Chosen as an additive approach over full footer replacement: avoids
reimplementing pi's token/cost/context-usage computation just to colour
them; pi's native stats remain intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous phrasing was canonical-loyalist Adeptus Mechanicus (Lex
Mechanicus, Omnissiah Be Praised, Christian cross ✠, Binary Is Blessed).
Retheming for Chaos-aligned Dark Mechanicum:
Compact banner:
L E X M E C H A N I C U S -> L E X H E R E T E K
OMNISSIAH BE PRAISED · DEUS EX MACHINA -> SCRAPCODE TRIUMPHS · BLOOD-FORGE ETERNAL
B I N A R Y I S B L E S S E D -> F L E S H I S W E A K
✠ (Christian cross) -> ✴ (eight-pointed Chaos Star)
Full banner left block analogously (LEX HERETEK / SCRAPCODE TRIUMPHS
/ BLOOD-FORGE ETERNAL / Flesh is weak. / The cogitator hungers.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- All [ai-server] / [markdown-body-color] / [mechanicus-thinking-label]
console.log calls now fire only when PI_DEBUG is set. Default boot is
clean.
- ai-server's discoverModels now filters out ids matching
/-\d+-of-\d+$/ — llama.cpp's --models-autoload registers every GGUF
shard as its own id, duplicating the preset's consolidated model.
These shard-named phantoms are no longer surfaced to pi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-generates a flavored session name on session_start when the
session is new and has no name yet. Format:
"<Adj>-<Noun> · <3-digit>"
e.g. "Sacred-Cogitation · 042", "Heretek-Datalink · 717".
18 adjectives × 18 nouns × 1000 = 324000 unique names. Auto-naming is
scoped to reason === "new"; resume/fork/reload/startup are untouched.
Command: /mechanicus-rename generates a fresh name for the current
session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Monkey-patches AssistantMessageComponent.updateContent to swap the
default "Thinking..." label for "Cogitating..." when reasoning blocks
are folded. Only replaces the default — if any code path sets its own
label, we leave it alone.
Override via PI_THINKING_LABEL env var.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: 100ms/frame × 20 frames = 400ms pulse, 2s quote dwell.
After: 250ms/frame × 40 frames = 1s pulse, 10s quote dwell.
Pulse still cycles dim → normal → bright → normal (4 shades) but each
shade now holds for 250ms, making the pulse slower and the glyph more
legible. Quotes now stay long enough to read comfortably.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces pi's default working spinner with a ⚙ that pulses through three
shades of gold (dim → normal → bright → normal, 100ms per frame ~ 400ms
heartbeat) next to a dark-mechanicum quote.
- 45 quotes across three categories (15 each):
pious / liturgical: "Cogitating..." / "Praise the Omnissiah." / ...
dark mechanicum: "Flesh is weak. Binary is strong." / ...
operational: "Parsing sacred data..." / "Cache blessed..."
- Each quote dwells for 5 pulse cycles (20 cog-frames = 2s) before
switching. Full deck runs ~90s; typical turns show 3-5 quotes.
- Pseudo-random: Fisher-Yates shuffle on session_start AND turn_start,
so each assistant response draws from a fresh order.
- Colors: warning/gold shades for the cog pulse, muted/bronze for text.
Hardcoded hex so pre-built frames don't bounce through theme.fg.
Commands: /dark-mechanicus-indicator-on, /dark-mechanicus-indicator-off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Theme: warpGreen #7a8a42 -> #5a6b2e (darker, more "nurgle", still
readable for syntaxString / success / toolDiffAdded).
- markdown-body-color: extend to also monkey-patch Editor.prototype.render
so typed-text and the cursor (reverse-video) inherit the same inkPurple
body color instead of falling through to the terminal-default fg.
Re-opens the color after \x1b[0m (cursor's full reset) and \x1b[39m
(nested theme.fg close) so color survives those breaks within a line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pi-tui's Markdown component only colors paragraph text when the caller
passes a defaultTextStyle.color wrapper. Pi-coding-agent does this for
thinking blocks but not for regular assistant content, so paragraphs
emit uncoloured and inherit the terminal profile's foreground color
(green on many themes).
This extension monkey-patches Markdown.prototype.render to install a
fallback defaultTextStyle (inkPurple #d4c5e8 by default, overridable
via PI_MARKDOWN_BODY_COLOR) when one isn't already set. Thinking blocks
and other custom-colored markdowns are untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setting text/userMessageText/customMessageText to "" means pi renders
with the terminal's default foreground color — which on many terminal
profiles is green, leaking through to the cursor (rendered via reverse-
video `\x1b[7m`), editor-typing text, autocomplete non-selected items,
and plain chat text.
Fix: introduce an `inkPurple` var (#d4c5e8, soft readable lavender,
~14:1 contrast on the #16101e background) and use it for every token
that was previously "":
- text "" -> inkPurple
- userMessageText "" -> inkPurple
- customMessageText "" -> inkPurple
- mdCodeBlock "" -> parchment (distinct from body, stays warm)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Embeds a 17-line × ~38-col cog-and-skull ASCII art.
- Character→theme mapping for color shading:
% accent (bloodRed — skull features)
# borderAccent (rust — dense shading)
* border (brass — main body)
+ warning (gold — highlights)
= muted (bronze — mid tones)
- dim (far edges)
- Full mode = art right-aligned + centered text block on the left
(LEX MECHANICUS / OMNISSIAH… / Binary is blessed.).
- Compact mode = the prior 7-line text-only banner, used as fallback
when terminal width < art + margin.
- Default on session_start: full (auto-falls-back to compact on
narrow terminals).
- Commands: /mechanicus-banner-full, -compact, -on, -off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces pi's default header (logo + keybind hints) with a centered
banner featuring:
- Brass frame with rust ⚙ cog bookends on each horizontal rail
- Top line: rust cog-tooth hints (▲▲▲) flanking gold ✠ crosses and a
blood-red "L E X M E C H A N I C U S"
- Middle line: gold ☠ skulls flanking a muted "OMNISSIAH BE PRAISED ·
DEUS EX MACHINA"
- Bottom line: dim "B I N A R Y I S B L E S S E D" between em dashes
Colors resolve through the active pi theme (border, borderAccent, accent,
warning, muted, dim) so the banner adapts if the theme changes, but it
was designed for the dark-mechanicus palette added earlier.
Toggle via /mechanicus-banner-on / /mechanicus-banner-off. Installed
automatically on session_start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- themes/midnight-orchid.json: custom dark purple theme. Deep aubergine
background (#1a1525), violet/magenta accents, sage/amber/cyan syntax
for contrast. All 51 required color tokens defined.
- scripts/install-client.sh: adds a themes sync step that rsync's the
repo's themes/ into ~/.pi/agent/themes/ on each new machine.
Activate with /settings in pi or by setting "theme": "midnight-orchid"
in ~/.pi/agent/settings.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/install-client.sh: bootstraps a pi client — fetches certs from
the Caddy host via scp, rsyncs the extensions into ~/.pi/agent/, sets
up SSH key-auth to the ai-server for admin commands, probes the mTLS
/health endpoint to verify.
- scripts/issue-client-cert.sh: run on the Caddy host to mint a new
device identity — generates key + CSR, signs with the local root CA,
and emits both a modern p12 (AES-256) and a -legacy p12 (3DES/RC2-40)
for NSS-based browsers.
- scripts/install-browser-certs.sh: imports certs into Brave Flatpak's
isolated NSS DB, ~/.pki/nssdb for packaged Chromium-family browsers,
each Firefox profile, optionally the system trust store, and
optionally drops a Brave AutoSelectCertificateForUrls policy so the
cert prompt stops appearing on every page load.
All three are idempotent, --help-aware, and accept env/flag overrides
for the hardcoded defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ai-server/: multi-file pi extension that talks to a remote llama.cpp
router over mTLS (custom streamSimple), with dynamic model discovery
and admin slash commands for load/unload/ctx-size/restart/preset.
Includes README.md documenting the full mTLS + systemd + Caddy setup.
- local-llama.ts: minimal extension registering a local llama.cpp server
as an OpenAI-compatible provider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>