- Extract version-checking logic into src/utils/update-checker.ts (shared
between CLI and TUI)
- Updating routstrd...
bun add v1.2.22 (6bafe260)
installed routstrd@0.3.10 with binaries:
- routstrd
[649.00ms] done
routstrd updated successfully.
Updating cocod...
bun add v1.2.22 (6bafe260)
installed @routstr/cocod@0.0.24 with binaries:
- cocod
[692.00ms] done
cocod updated successfully.
Both routstrd and cocod have been updated!
Using remote daemon — skipping routstrd daemon restart.
cocod daemon was not running — skipping restart.
✓ All daemons restarted successfully. now checks npm for the latest version of each package
and only reinstalls when a newer version is available; skips daemon
restart when nothing was updated
- TUI shows a bold yellow 'UPDATE AVAILABLE' banner below the header on
all tabs when a new version is detected
- TUI checks for updates at most every 210 minutes to avoid spamming the
npm registry; first check fires 3s after startup (non-blocking)
- Bump version to 0.3.11
- Redirect detached daemon stdout/stderr to ~/.routstrd/debug.log
instead of ignoring them, so uncaught exception stack traces are
no longer lost
- Add process-level uncaughtException/unhandledRejection handlers
that log to the file logger before the process dies
- Wrap setInterval async callbacks (model refresh + refund jobs) in
IIFE catch chains so rejected promises can't escape and kill the
process silently
The update command previously only downloaded and installed new
binaries for routstrd and cocod without restarting the running
daemons, so updates would not take effect until a manual restart.
- Add restartDaemonsAfterUpdate() helper that gracefully stops and
restarts both daemons after a successful update
- routstrd: uses POST /stop (drains active connections), polls for
shutdown, then calls startDaemon() with configured port/provider
- cocod: runs 'cocod stop', then spawns 'cocod daemon' detached,
polls 'cocod ping' until it comes back up
- Skips restart for daemons that weren't running
- Skips routstrd daemon restart when using a remote daemon
- Collects and reports failures without rolling back the update
- Bump @routstr/sdk to 0.3.15
- Bump routstrd version to 0.3.7
Closes nostr task: update-restart
renderToday declared todayStats/recentDays/hourlyMap with let but only
assigned inside if (stats.summary); TypeScript flagged them as used
before assignment (TS2454). Initialize with sensible defaults so the
function degrades gracefully when summary is missing.
The usage-summary tz-bucketing test used hardcoded May 2026 timestamps
with a comment dated 2026-06-02. Those entries aged out of
getUsageSummary's 30-day rolling window, so days came back empty.
Recompute timestamps relative to now and assert on dynamically-derived
local-day date strings.
Calls refreshNostrEvents() after initial bootstrap and in the 21-minute
recurring job, covering provider discovery (38421) and lgtm reviews
(38425) which were previously only refreshed on manual trigger.
Split the monolithic render() into two distinct functions:
- fetchData(): async background fetch that updates state and triggers
a repaint, guarded against overlapping calls
- render(): synchronous paint that reads current state and writes to
stdout, safe to call from key handlers without blocking
Run all four daemon calls concurrently via Promise.all to cut the
blocked window. Remove redundant isDaemonRunning() checks from each
fetch function in data.ts — the single check now lives in fetchData().
Key handlers now call the sync render() directly instead of
void render(false), so scrolling and tab switching feel instant.
The SDK dropped minTotalTokens/maxTotalTokens from AggregateUsageOptions
(routstr-sdk c98de6b), so replace the five aggregate() calls with a single
list() and bucket entries in-process.
routstrd providers list --refresh now:
- Re-fetches Nostr kind 38421 provider discovery events
- Re-fetches Nostr kind 38423 routstr21 model list
- Re-fetches Nostr kind 38425 review events (applies LGTM-based disable)
- Fetches models from all discovered providers
- Syncs fresh provider list and disabled status into the store
Usage: routstrd providers list --refresh
- Import ModelManager from @routstr/sdk/bun instead of @routstr/sdk
- Import storage helpers from @routstr/sdk/storage/bun instead of @routstr/sdk/storage
- Fix createBunSqliteUsageTrackingDriver call: async and no longer needs
manual bun:sqlite import (now handled internally by the sdk entrypoint)
- Verified sharded discovery adapter setup matches SDK pattern in
scripts/routstr-daemon.ts at 6077aa7
- Add optional contextWindow field to PiModelEntry type
- Map model.context_length to contextWindow when available
- Add context_length to RoutstrModel type in registry
Adds an authUrl field to RoutstrdConfig so CLI management commands
(npubs, clients, usage) can be directed to the routstrd-auth proxy
instead of the daemon. This fixes 'routstrd npubs list' failing with
'Only POST is supported' when the auth proxy and daemon run on
separate ports (e.g., Cloudron setup: proxy on 8008, daemon on 8009).
Changes:
- config.ts: add authUrl to RoutstrdConfig
- daemon-client.ts: add getAuthBaseUrl(), callAuth(), refactor callDaemon
- cli.ts: npubs/usage commands use callAuth, remote command accepts --auth-url
- clients.ts: clients commands use callAuth
- Add cooldownMinutes field to /nwc/status and /nwc/auto-refill responses
- Change auto-refill log from ms to minutes display
- Keep cooldownMs internally for calculations
Two bugs caused the auto-refill loop to retry every 5s regardless of outcome:
1) Backoff used lastRefillAt which was only set on success. On failure
it stayed at 0, so now - 0 < backoffInterval never blocked once the
daemon had been running > 30s. Switched to a separate lastAttemptAt
that is updated on every attempt (success or failure).
2) preimage.slice() crashed when NWC returned a payment result without
a preimage field. This turned successful payments into exceptions,
triggering retries and causing the wallet to be charged repeatedly.
Guard preimage with an if-check before calling .slice().
- Add reconnect() method to wallet adapter that tears down old
WalletConnect/RelayPool and creates a new one on the fly
- Auto-refill loop now uses a getWallet() getter so it always
references the latest wallet instance after reconnect
- POST /nwc/connect and /nwc/disconnect handlers call reconnect()
after saving config, no restart needed
- Remove 'Run routstrd restart' messages from CLI connect/disconnect
Changed startAutoRefillLoop to accept a config getter instead of a static
config object. The getter reads config from disk on every check cycle, so
CLI changes (nwc auto-refill on/off) take effect immediately.
- auto-refill.ts: config parameter replaced with getConfig() callback
- wallet/index.ts: always start loop when wallet exists, use getter
- daemon/index.ts: pass getAutoRefillConfig reading from disk sync
- config-store.ts: added loadDaemonConfigSync() for sync disk reads
- http/index.ts + cli.ts: removed 'Restart daemon to apply' messages
- Add step-by-step logging to fundFromNWC (initial/final balance, invoice,
payment status, fees) matching pay_invoice.mts pattern
- Remove invoice and preimage from POST /nwc/fund response
- Add final wallet balance to the response after funding completes
- auto-refill.ts: use WalletConnect instance directly instead of
NwcClient + RelayPool; check wallet.service for connection state
- wallet/index.ts: create single shared WalletConnect for auto-refill,
fundFromNWC, and getNwcStatus; remove NwcClient entirely
- daemon/index.ts: strip all NwcClient creation/connect/logging/cleanup;
auto-refill now gates on connectionString alone
- add applesauce-wallet-connect dependency
- Encryption auto-detection: reads wallet info event (kind 13194) on connect
to choose NIP-04 vs NIP-44 per NIP-47 spec
- Subscription readiness: wait for EOSE before resolving connect()
- Dual NIP-04 / NIP-44 encrypt/decrypt support
- Proper encryption tags on NIP-44 requests
- test_nwc.ts: standalone NWC test script for make_invoice + pay_invoice
Two bugs caused NWC responses to never arrive:
1. getPublicKey() in nostr-tools v2 already returns a hex string, but
the code wrapped it with Buffer.from().toString('hex'), producing a
double-encoded 128-char string. The relay rejected the REQ filter
with 'filter item too large' because the #p tag value was invalid.
2. The 'secret' from the NWC connection string (the pre-shared client
private key per NIP-47) was parsed but never used. Instead a new
random keypair was generated on every restart, so the wallet service
would see requests from an unexpected pubkey.
Also removed 'since' from the subscription filter (simpler, some relays
choke on it) and added debug logging for the REQ filter.
- Add Dockerfile, docker-compose.yml, and .dockerignore for Bun dev container
- Fix sdkLogger to use logger.warn instead of logger.log for warnings
- Add proper warn method to logger (writes to file instead of console)
- Use console.log for daemon startup messages to keep them visible
- Update cashu-ts getDecodedToken call with explicit well-known-pubkeys
- Use proof.amount.toNumber() for updated cashu-ts Amount type
- daemon/index.ts: add makeSdkLogger() factory, wire daemonSdkLogger into
createBunSqliteDriver, createProviderRegistryFromStore, ModelManager, ProviderManager
- daemon/http/index.ts: add makeSdkLogger() factory, generate per-request
reqId via randomBytes(4), pass reqLogger = sdkLogger.child("req:${reqId}")
to routeRequests so every log line from a request is prefixed [req:XXXXXXXX]
- start-daemon.ts: remove >> logfile 2>&1 redirect and getTodayLogFile();
all file logging now goes through the daemon's writeLog() with daily rotation
Result: Every log line from a request is prefixed [req:XXXXXXXX], all entries
have [ISO] [INFO/ERROR/DEBUG] timestamps, no duplicates, and the log file
rotates correctly at midnight.
- Add 'npubs' tab between Clients and Recent (key 7)
- Tab is hidden when no clients have ownerNpub set
- Groups usage entries by ownerNpub via client-id → npub mapping
- Shows truncated npub (first 10 + … + last 6) with full npub on detail line
- Displays requests, cost, tokens, avg cost per npub with bar charts
- Includes 'Top Models per Npub' sub-section
- Tab keys dynamically renumbered (1-7 without npubs, 1-8 with)
- Falls back to Clients tab if Npubs becomes unavailable on refresh
Adds a CLIENT column between TIME and MODEL in the Recent Requests tab,
color-coded using CLIENT_COLORS. Entries without a client show "unknown".
Closes#25
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After bootstrapProviders runs, newly discovered URLs (e.g. privateprovider.xyz)
were only known to the ModelManager cache and never written into the store's
baseUrlsList. This caused `providers list` and `models -m <id>` to show
different sets of providers. Now the bootstrap step merges any new URLs into
the store so both commands draw from the same source of truth.
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
When cocod cannot assemble enough proofs to send (e.g. due to
denomination fragmentation or stale proof state), it returns
"Send failed: Not enough proofs to send". This was propagating
as a generic Error, causing the HTTP handler to return 500 instead
of the structured 402 insufficient_balance response.
Map that cocod error to InsufficientBalanceError in walletAdapter.sendToken
so the existing handler path returns the correct 402.
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
- Updated npubs list to display role (admin/user) for each npub
- Changed header from 'Admin npubs' to 'Npubs' to reflect all roles
- Updated command descriptions to reflect broader scope
- Also fixed 'admin npub' message in add command output
The auth proxy intercepts GET /usage and returns only entries belonging to
the authenticated npub's clients, with owner suffixes stripped. The TUI no
longer needs to filter entries client-side.
- Remove getNpubSuffix and loadConfig from tui/usage/data.ts imports
- Remove old suffix-based filtering logic from fetchUsage()
- Remove unused getNpubSuffix from cli.ts imports
- Add ownerNpub?: string to ClientEntry and DaemonClient interfaces
- Update getClientsFromStore() to preserve ownerNpub field
- GET /clients: include ownerNpub in daemon response
- POST /clients/add: accept and store ownerNpub from auth proxy
- Remove getNpubSuffix/addSuffixToId/removeSuffixFromId from clients.ts (moved to auth proxy)
- Remove CLI-side suffixing in addDaemonClient and deleteClientAction
- Remove config loading from getClientsList (no longer needed for filtering)
- CLI output now shows raw client IDs without suffix stripping
- Add new installHermesIntegration to write ~/.hermes/config.yaml
- Prepend model block with base_url/api_key/default model at top
- Append custom_providers block at the bottom
- Use 3rd model in daemon list as default
- Add Hermes to setup menu and registry
When multiple routstrd processes (or CLI auto-starts) bootstrap simultaneously,
each process found cocod/routstrd unreachable, spawned its own copy, and
contended for the same database socket. The paired ~200ms burst timestamps
in the logs were the symptom.
Fixes applied:
- Add cross-process startup lock (process-lock.ts)
- Uses atomic mkdir as the lock primitive.
- Stores PID + UUID token; verifies ownership on release to avoid
removing a lock taken by a new process after the original crashed.
- Removes stale locks (unreachable PID or lock older than staleAfterMs).
- Exports withCrossProcessLock() for simple RAII-style usage.
- Fix cocod startup race (cocod-client.ts)
- Wrap ensureDaemonRunning() in a socket-derived lock path.
- Re-ping inside the lock before spawning; if another process just started
cocod, the current process detects it and connects instead of spawning.
- Switch from 'cocod daemon' to 'cocod init' for cleaner daemonization.
- Allow exit code 0 from 'cocod init' (normal daemon exit without error).
- Consolidate routstrd daemon startup (daemon-client.ts)
- startDaemonProcess() now delegates to startDaemon() from start-daemon.ts,
sharing the same lock and health-check logic.
- Add cross-process lock to routstrd daemon startup (start-daemon.ts)
- Performs pre-lock isDaemonHealthy() check to skip lock entirely when
already running.
- Performs re-check inside the lock before spawning to avoid duplicate
daemon processes.
- Added 'routstrd refresh' command that:
- Calls /v1/models?refresh=true to refresh routstr21 models
- Calls runIntegrationsForClients() for all registered clients
- Fixed type signature in refreshModelsAndIntegrations (returns any[] not void)
- Imports getClientsList and runIntegrationsForClients in cli.ts
- Created reusable refreshModelsAndIntegrations() function that combines
model refresh with client integration refresh
- Updated src/daemon/index.ts to use the new function in both the scheduled
refresh job and the initial bootstrap refresh
- Merged two chained .then() calls into one in the initial bootstrap flow
- Removed redundant imports (runIntegrationsForClients, getClientsList) from
daemon/index.ts since they're now encapsulated in the function