From 5b4ddf79c4acd65827e60f1e49535d3af4442b4c Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 9 Sep 2026 14:49:25 -0500 Subject: [PATCH] chore(docs): remove shipped planning docs, refresh CLI reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of the repo's non-source documentation and ad-hoc scripts. Deleted (work already shipped, or scratch files): - IMPLEMENTATION.md — the ~/.cocod → ~/.routstrd/wallet migration plan; shipped as src/daemon/wallet/{migration,paths}.ts. - src/TUI refactor.md — plan to move src/cli/usage-tui.ts into src/tui/; done, src/cli/ no longer exists. - v1-messages-format-report.md — the Messages-API passthrough bug it describes is fixed; http/index.ts now forwards path: url.pathname. - routstr-cost-logging.md — note about the routstr proxy's cost fields vs pi-ai; not about this codebase. - refund.js, refund_new.js — one-off scripts with hardcoded Cashu tokens. - test_box.ts, test_split_box.ts, test_split_box2.ts — manual renderBox eyeball checks that asserted nothing and bun test never ran. Moved: - COCO-2.0.0-MIGRATION-PLAN.md → docs/plans/coco-2.0.0-migration.md, with a status header. This one is genuinely unexecuted: package.json is still on coco-core 1.0.1 / coco-cashu-core 1.1.2-rc.50, and its NPC compatibility gate is unresolved. Added: - src/tui/usage/render.test.ts — real assertions for what the three deleted scratch scripts checked by eye (box width with and without a title, ANSI padding, side-by-side composition including unequal heights). renderBox had no coverage before. Updated: - SKILL.md — ships in the npm package as the CLI reference but was missing ~15 commands (wallet doctor/cleanup/npc, nwc and auto-refill, history, ping, providers reviews, service install/uninstall/logs, daemon, local, update, refund, top, send/receive shortcuts). Also dropped the claim that onboard installs cocod (the wallet is in-process), removed two orphaned tables misfiled under `routstrd refresh`, and completed the config and env-var tables. - README.md — same cocod correction, points at SKILL.md for the full command reference, fixes the stale Project Structure tree and the POST / endpoint description. - SECURITY.md — drop the stray `## SECURITY.md` line above the real heading. - package.json — add a `smoke` script so scripts/smoke/chat-completions.sh stops looking orphaned. Deliberately not wired into CI: it needs a funded API key. Note: the Cashu tokens in the deleted refund scripts remain in git history. They date from March/April 2026 and were being POSTed to refund endpoints, so they are almost certainly spent — flagging rather than rewriting history. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UW5RBn7pxSuHbDQjtHfQVW --- IMPLEMENTATION.md | 253 --------------- README.md | 47 ++- SECURITY.md | 1 - SKILL.md | 288 ++++++++++++++---- .../plans/coco-2.0.0-migration.md | 6 + package.json | 1 + refund.js | 33 -- refund_new.js | 20 -- routstr-cost-logging.md | 71 ----- src/TUI refactor.md | 113 ------- src/tui/usage/render.test.ts | 80 +++++ test_box.ts | 15 - test_split_box.ts | 17 -- test_split_box2.ts | 23 -- v1-messages-format-report.md | 223 -------------- 15 files changed, 357 insertions(+), 834 deletions(-) delete mode 100644 IMPLEMENTATION.md rename COCO-2.0.0-MIGRATION-PLAN.md => docs/plans/coco-2.0.0-migration.md (98%) delete mode 100644 refund.js delete mode 100644 refund_new.js delete mode 100644 routstr-cost-logging.md delete mode 100644 src/TUI refactor.md create mode 100644 src/tui/usage/render.test.ts delete mode 100644 test_box.ts delete mode 100644 test_split_box.ts delete mode 100644 test_split_box2.ts delete mode 100644 v1-messages-format-report.md diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md deleted file mode 100644 index 9e1f571..0000000 --- a/IMPLEMENTATION.md +++ /dev/null @@ -1,253 +0,0 @@ -# Migrate wallet storage from `~/.cocod` to `~/.routstrd/wallet` - -Task: [`routstrd-migrate-cocod-to-routstrd-files`](https://github.com/nodestrich/routstrd) -Event ID: `2f420644005154c5b106017c4a753854bb2660ba834cde7e6e267532cb5448e9` -Priority: 997 · Status: open - ---- - -## Baseline - -This plan is based on `coco-integration` at `86ac688` (after the default-mint, NPC/npubx.cash, and Windows-support changes). - -The task branch is currently based on `04f8c22`; rebase it onto the latest `coco-integration` before implementing this plan. - -## Goal - -Move the in-process Cashu wallet's persistent data from: - -```text -~/.cocod/config.json -~/.cocod/coco.db -``` - -to: - -```text -~/.routstrd/wallet/config.json -~/.routstrd/wallet/coco.db -``` - -The migration must preserve the mnemonic, proofs, default mint, and NPC identity. Existing users must not accidentally get a new wallet, and `cocod` and routstrd must never open the same database concurrently. - -Fresh installs must use `~/.routstrd/wallet` immediately. Existing installs must be migrated automatically and safely; merely continuing to run from `~/.cocod` is not sufficient for this task. - ---- - -## Important corrections to the previous plan - -### 1. Do not rename or migrate `cocod.sock` - -The in-process `coco-core` wallet does not expose a Unix socket. `cocod.sock` belongs to the legacy external `cocod` daemon and is used only to determine whether that daemon is still alive. - -There should therefore be no `wallet.sock`. Do not copy a socket inode into the new directory. - -### 2. Keep legacy-process paths separate from wallet-data paths - -On the current base, `src/daemon/wallet/coco-client.ts` derives all of these from one `CONFIG_DIR`: - -- wallet config and database; -- legacy cocod socket; -- legacy cocod PID/process lock. - -That coupling must be removed. After migration: - -- wallet data comes from `~/.routstrd/wallet`; -- probes and shutdown logic for an external legacy cocod continue to use `~/.cocod/cocod.sock` and `~/.cocod/cocod.pid`; -- routstrd uses `~/.routstrd/wallet/wallet.pid` as the in-process wallet lock. - -While routstrd owns the migrated wallet, it should also retain the existing legacy-cocod exclusion mechanism (claiming the legacy `cocod.pid`) so an old cocod cannot be started against a stale or partially migrated legacy wallet. Acquisition and release of both locks must be rollback-safe. - -### 3. Do not rename `cocodPath` - -`RoutstrdConfig.cocodPath` is an executable path for the external compatibility client, not a wallet storage path. Renaming it to `walletPath` would change its meaning and would not help this migration. - -The current daemon creates `createCocoClient()` directly and injects it into `createWalletAdapter()`, so `cocodPath` is effectively bypassed on the normal in-process path. Cleanup or removal of that compatibility setting is a separate task. - -### 4. Do not implement fallback as the steady state - -A resolver that permanently falls back to `~/.cocod` does not move the data. Legacy detection is needed to initiate migration, but successful startup should resolve to the canonical directory afterward. - -### 5. Account for the updated base - -The latest `coco-integration` adds: - -- `defaultMintUrl` persistence in the wallet's `config.json`; -- the NPC plugin, whose Nostr identity is derived from the same mnemonic; -- `src/daemon/wallet/coco-client.npc.test.ts`; -- Windows support and `USERPROFILE` fallback. - -Migration must preserve the complete config JSON rather than reconstructing selected fields. Path construction must use `path.join` rather than hard-coded `/` separators. - ---- - -## Current runtime path references on the latest base - -### Wallet data and legacy process coordination - -| File | Current responsibility | Required change | -|---|---|---| -| `src/daemon/wallet/coco-client.ts` | Uses one `.cocod` directory for config, DB, socket, and PID; owns the in-process wallet and legacy-cocod guard | Split canonical data paths from legacy process paths; migrate before opening the DB; use `wallet.pid` for the in-process lock | -| `src/daemon/wallet/cocod-client.ts` | External cocod compatibility client; defaults to `.cocod/cocod.sock` | Keep legacy defaults and clarify that they are external-cocod paths | -| `src/cli.ts` | Initializes `.cocod`; two restart paths wait on `.cocod/cocod.pid` | Initialize/migrate the canonical wallet and wait on `wallet.pid` | -| `src/utils/config.ts` | Defines `~/.routstrd`, but no wallet subdirectory | Export canonical and legacy wallet/process path helpers or constants | -| `src/daemon/index.ts` | Calls `createCocoClient()` before building the adapter | Ensure migration occurs before the client opens the DB | - -### Tests and documentation - -| File | Required update | -|---|---| -| `src/cli.test.ts` | Change fresh-wallet expectations and add migration coverage | -| `src/daemon/wallet/coco-client.test.ts` | Update data/lock paths and add migration/dual-lock tests | -| `src/daemon/wallet/coco-client.npc.test.ts` | Use the canonical wallet layout in fixtures; verify migrated mnemonic/config still drives NPC identity | -| `README.md` | Document the new location and migration behavior; retain legacy cocod terminology only where discussing compatibility | -| `SKILL.md` | Update wallet storage and environment-variable documentation | - -The fixture `src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz` keeps its name because it records the fixture's provenance. - ---- - -## Target path model - -Define the path model in one dependency-light module (for example `src/daemon/wallet/paths.ts`, or in `src/utils/config.ts` if that does not introduce a cycle): - -```text -ROUTSTRD config root process.env.ROUTSTRD_DIR || ~/.routstrd -canonical wallet directory process.env.ROUTSTRD_WALLET_DIR || /wallet -canonical wallet config /config.json -canonical wallet database /coco.db -canonical wallet lock process.env.ROUTSTRD_WALLET_PID || /wallet.pid - -legacy cocod directory process.env.COCOD_DIR || ~/.cocod -legacy cocod socket process.env.COCOD_SOCKET || /cocod.sock -legacy cocod PID process.env.COCOD_PID || /cocod.pid -``` - -Use `HOME` with `USERPROFILE` fallback, matching the updated Windows-support base. Use functions or injectable path objects where tests need to change environment variables after module import. - -`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` remain compatibility controls for locating an old external cocod. They must not redirect the new in-process wallet away from `~/.routstrd/wallet`. `ROUTSTRD_WALLET_DIR` is the new explicit data-directory override. - ---- - -## Implementation plan - -### Phase 1 — Centralize and separate paths - -1. Add the canonical wallet and legacy cocod path definitions above. -2. Remove the local `.cocod` path construction from `coco-client.ts` and `cli.ts`. -3. Change `CreateCocoClientOptions` so tests/tooling can independently override: - - wallet data directory; - - wallet lock path; - - legacy cocod socket and PID paths. -4. Keep `cocod-client.ts` pointed at the legacy socket. Do not make it import a resolver that prefers the new wallet directory. - -### Phase 2 — Add a safe automatic migration primitive - -Add an idempotent `migrateLegacyWallet()` helper with injectable paths/filesystem operations for tests. - -Preconditions and behavior: - -1. If the canonical wallet already contains both `config.json` and `coco.db`, return `already-current` and do not touch legacy data. -2. If neither canonical nor legacy wallet data exists, return `fresh`; initialization will create the canonical directory. -3. If only the legacy wallet exists: - - first verify that legacy cocod is not running using the existing PID and socket guard; - - create a private staging directory under `~/.routstrd` with mode `0700`; - - copy the complete `config.json` and `coco.db` into staging without parsing or rewriting them; - - apply `0600` to both files; - - validate that the staged files exist and have the expected byte sizes; - - atomically rename the staged directory to `wallet`; - - only after the canonical directory is committed, remove the legacy `config.json` and `coco.db`; - - never copy `cocod.sock` or `cocod.pid`. -4. If canonical storage is partial, legacy storage is partial, both contain wallet data, or files conflict, stop with an actionable error. Never merge databases and never generate a new mnemonic over an ambiguous state. -5. Clean up an uncommitted staging directory after failure. A committed canonical wallet remains authoritative if cleanup of the old files fails; report that cleanup warning clearly. - -The migration should preserve unknown config fields, including `defaultMintUrl`, and preserve the database byte-for-byte. This also preserves the NPC identity because that identity is derived from the mnemonic. - -A staging copy plus atomic directory rename is preferred over two independent file renames: a crash must not expose a half-created canonical wallet. Copying also permits a clear rollback before commit and supports a legacy directory located on another filesystem through `COCOD_DIR`. - -### Phase 3 — Run migration from every wallet-opening path - -1. **CLI onboarding/init:** run migration before `initializeWallet()`. Only initialize a new mnemonic when migration reports `fresh`. -2. **Daemon direct startup:** run migration before `createCocoClient()` opens `coco.db`. This covers users who invoke the daemon without rerunning onboarding. -3. **Start/restart/update/service paths:** retain `stopLegacyCocod()` before migration/startup where those paths already stop legacy cocod. Direct daemon startup should refuse with the existing actionable error rather than silently running two wallet engines. -4. Print a concise success message showing old and new directories, but never print config contents or the mnemonic during migration. - -A separate `routstrd wallet migrate` command is optional as a manual recovery/preview entry point, but it must call the same migration primitive. It is not a substitute for automatic migration. - -### Phase 4 — Separate process locking - -Refactor the current `claimLegacyCocodPidFile()` behavior into explicit responsibilities: - -1. Claim `wallet.pid` for the lifetime of the in-process wallet to prevent two routstrd wallet instances from opening `coco.db`. -2. Continue claiming the legacy `cocod.pid` while routstrd is active, after verifying no real cocod owns it, to prevent an old cocod from starting. -3. If either claim fails, release any claim already acquired before returning the error. -4. On `CocodClient.dispose()`, startup failure, and daemon shutdown, release only PID files still owned by the current process. -5. Keep `stopLegacyCocod()` and `assertLegacyCocodNotRunning()` operating only on legacy cocod paths. -6. Update both PID-release waits in `src/cli.ts` (`restartDaemonsAfterUpdate` and `restart`) to wait for the canonical `wallet.pid` rather than `.cocod/cocod.pid`. - -There is no canonical wallet socket. - -### Phase 5 — Update initialization and client defaults - -1. Make `initializeWallet()` default to the canonical wallet directory. -2. Preserve directory mode `0700` and config mode `0600` on both migrated and fresh wallets. -3. In `createCocoClient()`, derive `config.json` and `coco.db` from the canonical wallet directory, but take legacy guard paths separately. -4. Update the wallet-access comment near `unlock()` to reference `~/.routstrd/wallet/config.json`. -5. Leave `CocodClient`, `resolveCocodExecutable()`, and `cocodPath` compatibility behavior unchanged. - -### Phase 6 — Tests - -Add or update tests for: - -- fresh initialization creates `~/.routstrd/wallet`, not `.cocod`; -- a legacy config and database migrate byte-for-byte; -- `defaultMintUrl` and unknown config fields survive migration; -- the NPC-derived identity is unchanged after migration; -- migration never copies socket or PID files; -- migration refuses while a real legacy cocod is running; -- stale legacy socket/PID handling remains safe; -- an existing complete canonical wallet wins without modifying it; -- partial canonical, partial legacy, and conflicting dual-wallet states fail without generating a mnemonic; -- staging cleanup and retry after an interrupted migration; -- custom `ROUTSTRD_DIR`, `ROUTSTRD_WALLET_DIR`, and legacy `COCOD_DIR` paths; -- Windows-compatible path construction; -- acquiring the second process lock rolls back the first on failure; -- `dispose()` releases both owned locks and does not unlink another process's lock; -- both CLI restart paths wait for `wallet.pid`. - -Keep `cocod-0.0.24-wallet.db.gz` unchanged and continue using it to prove the migrated database opens successfully with the current in-process wallet. - -### Phase 7 — Documentation - -Update `README.md` and `SKILL.md` to state: - -- wallet data is stored in `~/.routstrd/wallet`; -- existing `~/.cocod` data is migrated automatically on first startup; -- users must back up the mnemonic before migration; -- `ROUTSTRD_WALLET_DIR` overrides the canonical wallet directory; -- `COCOD_DIR`, `COCOD_SOCKET`, `COCOD_PID`, and `cocodPath` refer only to legacy external-cocod compatibility. - -Do not replace every use of the word `cocod`: references to the external daemon, compatibility client, legacy guard, package, and fixture are still accurate. - ---- - -## Acceptance criteria - -- A fresh install creates wallet data only under `~/.routstrd/wallet`. -- Starting from a valid `.cocod` wallet results in the same config and database under the canonical directory without changing balances, mnemonic-derived NPC identity, or default mint. -- No startup path silently creates a new mnemonic when recoverable legacy data exists. -- A running legacy cocod blocks migration and database opening. -- The new wallet directory contains no copied Unix socket and uses `wallet.pid` only as an in-process lock. -- Legacy cocod probing and exclusion continue to use `.cocod/cocod.sock` and `.cocod/cocod.pid`. -- The implementation works with `USERPROFILE`/Windows path construction and custom directory overrides. -- All wallet, CLI, typecheck, and build tests pass on the rebased `coco-integration` branch. - ---- - -## Suggested commit sequence - -1. `refactor(wallet): separate routstrd wallet paths from legacy cocod paths` -2. `feat(wallet): atomically migrate legacy cocod wallet data` -3. `fix(wallet): use independent routstrd and legacy cocod process locks` -4. `test(wallet): cover migration, conflicts, NPC identity, and lock rollback` -5. `docs: document routstrd wallet storage and legacy migration` diff --git a/README.md b/README.md index 0d490cb..841cf32 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # routstrd -Routstr daemon - A CLI tool for managing routstr processes, similar to `cocod` (a Cashu wallet daemon). +Routstr daemon - A CLI tool for managing routstr processes, with a built-in Cashu wallet. ## Overview -routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It integrates with `cocod` for wallet management and uses the Routstr SDK to handle provider routing and model discovery. +routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It carries an in-process Cashu wallet (coco) for payments and uses the Routstr SDK to handle provider routing and model discovery. ## Routstr for Teams @@ -13,7 +13,7 @@ For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd- ## Features - **Daemon Mode**: Run routstrd as a background HTTP server -- **Wallet Integration**: Works with cocod for Cashu token management +- **Wallet Integration**: In-process Cashu wallet, with Lightning, NWC, and NPC support - **Provider Routing**: Automatically discovers and routes requests to available providers - **Config Management**: Stores configuration in `~/.routstrd/` @@ -74,6 +74,9 @@ routstrd clients add --claude-code # or --pi-agent / --opencode > **Tip:** You can also install the [routstrd skill](https://github.com/Routstr/routstrd/blob/main/SKILL.md) so the agent can manage routstrd for you. ## More Commands + +[`SKILL.md`](SKILL.md) is the complete per-command reference — every command, +subcommand, and flag. The sections below cover the common ones. ### Start Daemon Start the background daemon: @@ -179,9 +182,13 @@ daemon restart is required. #### Route Request ``` -POST / +POST /v1/chat/completions ``` +Any unmatched `POST` path is proxied to the selected provider with the incoming +path preserved, so `POST /v1/messages` (Anthropic Messages API) and +`POST /v1/responses` (OpenAI Responses API) work in their own formats too. + Request body: ```json { @@ -241,6 +248,8 @@ overrides the 21-minute interval. - `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`) - `ROUTSTRD_SOCKET` - Socket path (default: `~/.routstrd/routstrd.sock`) - `ROUTSTRD_PID` - PID file path (default: `~/.routstrd/routstrd.pid`) +- `ROUTSTRD_WALLET_DIR` - Wallet data directory (default: `~/.routstrd/wallet`) +- `COCOD_DIR` - Legacy external cocod directory, used only for migration and exclusion (default: `~/.cocod`) ## Development @@ -249,14 +258,19 @@ Install dependencies: bun install ``` -Run CLI: +Run the CLI from source: +```sh +bun src/index.ts +``` + +Run the daemon: ```sh bun run start ``` -Run daemon: +Run the tests: ```sh -bun run start +bun test ``` Build a standalone executable for the current platform: @@ -298,7 +312,7 @@ more current model IDs to the smoke script: ```sh routstrd clients add --name smoke-test -ROUTSTRD_API_KEY= scripts/smoke/chat-completions.sh [model ...] +ROUTSTRD_API_KEY= bun run smoke [model ...] ``` Set `ROUTSTRD_BASE_URL` to test a daemon at a different address. The script @@ -327,12 +341,17 @@ not part of `bun test`. ``` routstrd/ ├── src/ -│ ├── index.ts # Entry point with shebang -│ ├── cli.ts # Commander CLI commands -│ ├── cli-shared.ts # IPC utilities -│ ├── daemon.ts # HTTP server daemon -│ └── utils/ -│ └── config.ts # Path configuration +│ ├── index.ts # CLI entry point with shebang +│ ├── cli.ts # Commander CLI commands +│ ├── daemon.ts # Compatibility daemon entrypoint (legacy PM2 registrations) +│ ├── start-daemon.ts # Daemon process launcher +│ ├── daemon/ # HTTP server, wallet, provider routing +│ ├── integrations/ # Client integrations (Claude Code, pi, OpenCode, ...) +│ ├── tui/ # Interactive usage monitor (`routstrd monitor`) +│ └── utils/ # Config, paths, daemon client, update checker +├── tests/ # Integration tests (unit tests sit beside their source) +├── scripts/smoke/ # Manual end-to-end smoke test +├── docs/plans/ # Design/migration plans not yet executed ├── package.json └── tsconfig.json ``` diff --git a/SECURITY.md b/SECURITY.md index ad9097b..11dbffa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,3 @@ -## SECURITY.md # Security Policy ## Reporting a Vulnerability diff --git a/SKILL.md b/SKILL.md index 3d0a0b8..988a14a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,27 +1,45 @@ # routstrd CLI Reference -Routstr daemon — a Bun-based CLI tool that runs a background HTTP server for the Routstr protocol. It integrates with `cocod` for Cashu wallet management and routes LLM requests to available providers. +Routstr daemon — a Bun-based CLI tool that runs a background HTTP server for the +Routstr protocol. It carries an in-process Cashu wallet (coco) for payments and +routes LLM requests to available providers. ## Quick Start ```sh -routstrd onboard # Initialize (creates config, sets up cocod) +routstrd onboard # Initialize (creates config, sets up the wallet) +routstrd receive 2100 # Top up over Lightning routstrd start # Start the daemon routstrd stop # Stop the daemon ``` -After onboarding, the daemon listens at `http://localhost:8008` and exposes an OpenAI-compatible API. +After onboarding, the daemon listens at `http://localhost:8008` and exposes an +OpenAI-compatible API. ## Commands ### `routstrd onboard` Initialize routstrd for the first time: -- Creates `~/.routstrd/` config directory -- Creates `~/.routstrd/config.json` with defaults (port 8008, apikeys mode) -- Installs `cocod` globally via bun if not present -- Runs `cocod init` to set up the wallet -- Starts the daemon and configures integrations +- Creates `~/.routstrd/` (mode 0700) and `~/.routstrd/config.json` (mode 0600) + with defaults (port 8008, host 127.0.0.1, apikeys mode) +- Generates a Nostr identity (`nsec`) for NIP-98 authentication +- Migrates a legacy `~/.cocod` wallet to `~/.routstrd/wallet` if one is present, + stopping any running external `cocod` first +- Initializes the in-process Cashu wallet +- Starts the daemon and configures a client integration + +| Option | Description | +|--------|-------------| +| `--opencode` | Set up OpenCode integration (non-interactive) | +| `--openclaw` | Set up OpenClaw integration (non-interactive) | +| `--pi-agent` | Set up Pi Agent integration (non-interactive) | +| `--claude-code` | Set up Claude Code integration (non-interactive) | +| `--hermes` | Set up Hermes integration (non-interactive) | +| `--skip-integration` | Skip integration setup | + +Use at most one integration flag. For several clients, run `routstrd clients add` +afterwards. `--skip-integration` cannot be combined with an integration flag. ### `routstrd start` @@ -30,28 +48,50 @@ Start the background daemon process. | Option | Description | |--------|-------------| | `--port ` | Port to listen on (default: 8008) | +| `--host ` | Bind address (default: 127.0.0.1) | | `-p, --provider ` | Default provider to use | +### `routstrd daemon` + +Run the daemon in the foreground (same options as `start`). Useful for debugging +and for process supervisors that expect a non-forking process. + ### `routstrd stop` Stop the background daemon. ### `routstrd restart` -Restart the daemon (stops if running, then starts). - -| Option | Description | -|--------|-------------| -| `--port ` | Port to listen on | -| `-p, --provider ` | Default provider to use | +Restart the daemon (stops if running, then starts). Same options as `start`. ### `routstrd status` Check daemon and wallet status. Returns JSON with current state. +### `routstrd ping` + +Test connectivity to the daemon. + ### `routstrd balance` -Get wallet and API key balances. Shows per-mint wallet balances, per-key API balances, and a grand total (all in sats). +Get wallet and API key balances. Shows per-mint wallet balances, per-key API +balances, and a grand total (all in sats). + +| Option | Description | +|--------|-------------| +| `--api-keys` | List all stored API keys (baseUrl + key + balance) | +| `--delete-api-keys ` | Delete the API key stored for a provider base URL (refunds its balance first) | +| `--mint-url ` | Mint to refund the deleted API key balance to (defaults to the active wallet mint) | + +### `routstrd refund` + +Refund pending tokens and API keys to a mint. + +| Option | Default | Description | +|--------|---------|-------------| +| `-m, --mint-url ` | active wallet mint | Mint URL to refund to | +| `-y, --yes` | false | Skip confirmation prompt | +| `--xcashu` | false | Refund xcashu tokens only | ### `routstrd models` @@ -60,6 +100,7 @@ List available routstr21 models (discovered via Nostr). | Option | Description | |--------|-------------| | `-r, --refresh` | Force refresh models from Nostr | +| `-m, --model ` | Show the providers serving a specific model | ### `routstrd usage` @@ -69,7 +110,19 @@ Show recent usage logs and total sats cost. |--------|---------|-------------| | `-n, --limit ` | 10 | Number of recent entries (max 1000) | -Shows timestamp, model, provider, sats cost, token counts, and request ID for each entry. +Shows timestamp, model, provider, sats cost, token counts, and request ID for +each entry. + +### `routstrd history` + +Show wallet transaction history. + +| Option | Default | Description | +|--------|---------|-------------| +| `-n, --limit ` | 50 | Number of entries to show | +| `--offset ` | 0 | Number of entries to skip | +| `-v, --verbose` | false | Show full details including encoded Cashu tokens | +| `--json` | false | Output raw JSON with token objects (no encoding) | ### `routstrd providers` @@ -77,7 +130,12 @@ List and manage providers (subcommand required). #### `routstrd providers list` -List all providers with their enabled/disabled status. Shows index, status, and base URL. +List all providers with their enabled/disabled status. Shows index, status, and +base URL. + +| Option | Description | +|--------|-------------| +| `--refresh` | Force re-fetch all Nostr events and refresh models from every enabled provider | ``` Providers (12 total, 2 disabled): @@ -103,6 +161,10 @@ Enable providers by their index numbers. routstrd providers enable 0 2 5 ``` +#### `routstrd providers reviews` + +Show all known providers with their stored review events and event IDs. + ### `routstrd clients` List and manage API clients (subcommand required). @@ -113,7 +175,11 @@ List and manage API clients (subcommand required). | `--disable-automatic-refresh` | Disable the daemon's scheduled refresh job | | `--enable-automatic-refresh` | Re-enable the daemon's scheduled refresh job | -The daemon refreshes models and client integrations on a schedule (every 21 minutes by default). Use `--manual-refresh` to do it on demand, and `--disable-automatic-refresh` to stop the scheduled job — the setting is stored in the daemon's `config.json` (`autoRefresh.enabled`) and takes effect without a restart. +The daemon refreshes models and client integrations on a schedule (every 21 +minutes by default). Use `--manual-refresh` to do it on demand, and +`--disable-automatic-refresh` to stop the scheduled job — the setting is stored +in the daemon's `config.json` (`autoRefresh.enabled`) and takes effect without a +restart. ```sh routstrd clients --manual-refresh # refresh models + integrations now @@ -125,7 +191,6 @@ routstrd clients --enable-automatic-refresh # scheduled refresh back on List all registered clients with their ID, name, API key, and creation date. - #### `routstrd clients add` Add a new client or set up a client integration. @@ -137,10 +202,11 @@ Add a new client or set up a client integration. | `--openclaw` | Set up OpenClaw integration | | `--pi-agent` | Set up Pi Agent integration | | `--claude-code` | Set up Claude Code integration | +| `--hermes` | Set up Hermes integration | ```sh routstrd clients add --opencode --pi-agent --claude-code # multiple integrations -routstrd clients add -n "My App" # generic client +routstrd clients add -n "My App" # generic client ``` Returns the client ID and API key for use with the OpenAI-compatible API. @@ -151,56 +217,58 @@ Delete a registered client by its ID. ### `routstrd npubs` -Manage registered npubs and their roles/names (subcommand required). Management commands route through the auth proxy (`--auth-url`) and use NIP-98 auth. +Manage registered npubs and their roles/names (subcommand required). Management +commands route through the auth proxy (`--auth-url`) and use NIP-98 auth. | Command | Description | |---------|-------------| | `routstrd npubs list` | List registered npubs with role and display name | | `routstrd npubs register [--name ]` | Register yourself as the first admin (bootstrap only) | -| `routstrd npubs add [--role ] [--name ]` | Add an npub (accepts hex or npub1...) | +| `routstrd npubs add [--role ] [--name ]` | Add an npub (accepts hex or npub1...); defaults to the `user` role | | `routstrd npubs update [--role ] [--name ]` | Update role and/or name (admin only) | | `routstrd npubs delete ` | Delete an npub | -### `routstrd remote ` +### `routstrd remote [url]` -Configure a remote daemon URL. Generates a Nostr identity (nsec/npub) for NIP-98 authentication automatically. +With no URL, print the configured remote daemon. Pass a URL to configure one — a +Nostr identity (nsec/npub) is generated automatically for NIP-98 authentication. + +| Option | Description | +|--------|-------------| +| `--auth-url ` | URL of the auth proxy used by management commands (`npubs`, `clients`, `usage`) | ```sh -routstrd remote https://your-remote-daemon.com +routstrd remote # show current remote +routstrd remote https://your-remote-daemon.com # configure one ``` +### `routstrd local` + +Switch back to local daemon mode (clears the configured remote daemon URL). + ### `routstrd refresh` -Refresh routstr21 models from Nostr and re-run integrations for all registered clients. Equivalent to `routstrd clients --manual-refresh`. +Refresh routstr21 models from Nostr and re-run integrations for all registered +clients. Equivalent to `routstrd clients --manual-refresh`. -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `port` | number | 8008 | Daemon HTTP port | -| `provider` | string\|null | null | Default provider URL | -| `daemonUrl` | string\|null | null | Remote daemon URL | -| `nsec` | string\|null | null | Nostr secret key for NIP-98 auth | -| `cocodPath` | string\|null | null | Custom path to cocod executable | -| `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) | -| `autoRefresh` | object | `{ enabled: true }` | Scheduled refresh job settings (`enabled`, `intervalMs`) | +### `routstrd update` -| Variable | Default | Description | -|----------|---------|-------------| -| `ROUTSTRD_DIR` | `~/.routstrd` | Config directory | -| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory | -| `ROUTSTRD_WALLET_PID` | `/wallet.pid` | In-process wallet lock path | -| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory | +Update routstrd to the latest version. Standalone-binary installs update +in place; npm/bun installs are updated through the package manager. ### `routstrd mode` Interactive prompt to set the client mode: -1. **lazyrefund/apikeys** (default) — Pseudonymous accounts kept with Routstr nodes, refunded after 5 mins if unused. -2. **xcashu** (coming soon) — Balances never kept with nodes, all refunded in response. +1. **lazyrefund/apikeys** (default) — Pseudonymous accounts kept with Routstr + nodes, refunded after 5 mins if unused. +2. **xcashu** (coming soon) — Balances never kept with nodes, all refunded in + response. Changing mode restarts the daemon automatically. -### `routstrd monitor` +### `routstrd monitor` / `routstrd top` -Open an interactive TUI (htop-like) for usage monitoring. +Open an interactive TUI (htop-like) for usage monitoring. `top` is an alias. ### `routstrd logs` @@ -211,17 +279,50 @@ View daemon logs. | `-f, --follow` | false | Follow log output (like `tail -f`) | | `-c, --coco` | false | Show Cashu wallet-engine (coco) logs instead of daemon logs | | `-n, --lines ` | 50 | Number of lines to show | +| `-r, --recent` | false | List recent request IDs with their model | +| `-i, --request-id ` | | Only show log lines for a specific request ID | -Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine (Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log` so they don't pollute the main daemon logs. +Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine +(Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log` +so they don't pollute the main daemon logs. + +### `routstrd service` + +Manage routstrd as a system service using PM2, so it survives reboots. + +| Command | Description | +|---------|-------------| +| `routstrd service install` | Install and start routstrd under PM2 | +| `routstrd service uninstall` | Stop and remove routstrd from PM2 | +| `routstrd service logs` | View PM2 logs for routstrd | ## Wallet Commands -New wallets automatically trust `https://mint.cubabitcoin.org` as their default mint. The default is used when a wallet command does not include `--mint-url`. +New wallets automatically trust `https://mint.cubabitcoin.org` as their default +mint. The default is used when a wallet command does not include `--mint-url`. + +### `routstrd send ` / `routstrd receive ` + +Shortcuts for the common wallet operations: + +| Command | Behaviour | +|---------|-----------| +| `routstrd send 2100` | Numeric target → create a Cashu token for that many sats | +| `routstrd send lnbc1...` | Non-numeric target → pay that Lightning invoice | +| `routstrd receive 2100` | Numeric value → create a Lightning invoice for that many sats and wait for payment | +| `routstrd receive cashuB...` | Non-numeric value → receive that Cashu token | + +Both accept `--mint-url `. ### `routstrd wallet status` Check wallet status. +### `routstrd wallet doctor` + +Diagnose conflicting wallets — the current routstrd wallet versus a legacy +`cocod` wallet. + ### `routstrd wallet unlock ` Unlock the wallet with a passphrase. @@ -230,6 +331,17 @@ Unlock the wallet with a passphrase. Get wallet balance. +### `routstrd wallet cleanup` + +Clear stuck pending/in-flight wallet operations. + +| Option | Default | Description | +|--------|---------|-------------| +| `--mint-url ` | all mints | Only clean up operations for this mint URL | +| `--min-age ` | 168 | Minimum age for reclaiming sends and cancelling melts (expired mint quotes are always failed) | +| `--dry-run` | false | Report what would be cleaned without applying changes | +| `-y, --yes` | false | Skip confirmation prompt | + ### `routstrd wallet receive cashu ` Receive funds via a Cashu token. @@ -274,6 +386,41 @@ Set the persistent default mint. If necessary, the mint is added as trusted firs Get info about a specific mint. +### `routstrd wallet npc` + +NPC (npubx.cash) Lightning address operations. + +| Command | Description | +|---------|-------------| +| `routstrd wallet npc address` | Show this wallet's NPC Lightning address | +| `routstrd wallet npc username [--confirm]` | Claim an NPC username; `--confirm` pays the claim fee | +| `routstrd wallet npc sync` | Manually sync paid NPC quotes into the wallet | + +## NWC (Nostr Wallet Connect) + +Connect an external Lightning wallet and let it fund the Cashu wallet. + +| Command | Description | +|---------|-------------| +| `routstrd nwc connect [connection-string]` | Connect via `nostr+walletconnect://...` (prompts if omitted) | +| `routstrd nwc disconnect` | Disconnect from the NWC wallet | +| `routstrd nwc status` | Show connection status and wallet info | +| `routstrd nwc fund ` | Manually fund the Cashu wallet from the connected NWC wallet | + +### `routstrd nwc auto-refill on` + +Enable automatic wallet refill from NWC. + +| Option | Default | Description | +|--------|---------|-------------| +| `--threshold ` | 500 | Refill when the Cashu balance drops below this | +| `--amount ` | 1000 | Refill this many sats at a time | +| `--cooldown ` | 300 | Minimum time between refills | + +### `routstrd nwc auto-refill off` + +Disable auto-refill. + ## Daemon API The daemon exposes an OpenAI-compatible HTTP API at `http://localhost:8008`: @@ -298,6 +445,10 @@ Route a chat completion request. } ``` +The incoming request path is forwarded to the provider, so the Anthropic +Messages API (`POST /v1/messages`) and the OpenAI Responses API +(`POST /v1/responses`) are proxied in their own formats as well. + ## Configuration Config file: `~/.routstrd/config.json` @@ -305,10 +456,20 @@ Config file: `~/.routstrd/config.json` | Field | Type | Default | Description | |-------|------|---------|-------------| | `port` | number | 8008 | Daemon HTTP port | +| `host` | string | `"127.0.0.1"` | Bind address | | `provider` | string\|null | null | Default provider URL | -| `cocodPath` | string\|null | null | Custom path to cocod executable | +| `cocodPath` | string\|null | null | Custom path to a legacy cocod executable | | `mode` | string | `"apikeys"` | Client mode (`apikeys` or `xcashu`) | +| `maxTokens` | number | 64000 | Completion budget applied when a client sets no output-token limit | +| `daemonUrl` | string | — | Remote daemon URL (set by `routstrd remote`) | +| `authUrl` | string | — | Auth proxy URL for management commands | +| `nsec` | string | — | Nostr secret key for NIP-98 auth | +| `relays` | string[] | — | Nostr relays to use for discovery | +| `routstrPubkey` | string | — | Override the Routstr announcement pubkey | +| `routstrModelsPubkey` | string | — | Override the routstr21 models pubkey | +| `nwc` | object | — | NWC settings (`mode`, `connectionString`, `autoRefill`) | | `autoRefresh` | object | `{ enabled: true }` | Scheduled refresh job settings (`enabled`, `intervalMs`) | +| `requestResponseLogging` | object | — | Request/response log sink settings | ### Environment Variables @@ -317,17 +478,26 @@ Config file: `~/.routstrd/config.json` | `ROUTSTRD_DIR` | `~/.routstrd` | Config directory | | `ROUTSTRD_SOCKET` | `~/.routstrd/routstrd.sock` | IPC socket path | | `ROUTSTRD_PID` | `~/.routstrd/routstrd.pid` | PID file path | +| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory | +| `ROUTSTRD_WALLET_PID` | `/wallet.pid` | In-process wallet lock path | +| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory | ## Remote Mode -When `daemonUrl` is configured, commands connect to a remote daemon instead of a local one: +When `daemonUrl` is configured, commands connect to a remote daemon instead of a +local one: - Client names are suffixed with the last 7 chars of your npub - All requests are automatically NIP-98 signed using your local nsec -- Local-only commands (`onboard`, `start`, `restart`, `mode`, `logs`, `service`) are disabled +- Local-only commands (`onboard`, `start`, `restart`, `mode`, `logs`, `service`) + are disabled + +Run `routstrd local` to switch back. ## Pi Integration -When `routstrd onboard` runs, it automatically configures a `routstr` provider in `pi`'s `models.json` with an OpenAI-compatible base URL and API key. This allows pi (the AI coding agent) to use Routstr providers seamlessly. +When `routstrd onboard` runs, it automatically configures a `routstr` provider in +`pi`'s `models.json` with an OpenAI-compatible base URL and API key. This allows +pi (the AI coding agent) to use Routstr providers seamlessly. ## File Locations @@ -337,5 +507,21 @@ When `routstrd onboard` runs, it automatically configures a `routstr` provider i | `~/.routstrd/routstr.db` | SQLite database | | `~/.routstrd/routstrd.sock` | IPC socket | | `~/.routstrd/routstrd.pid` | PID file | +| `~/.routstrd/wallet/` | In-process Cashu wallet data (`config.json`, `coco.db`, `wallet.pid`) | | `~/.routstrd/logs/YYYY-MM-DD.log` | Daily daemon log files | | `~/.routstrd/coco-logs/YYYY-MM-DD.log` | Daily Cashu wallet-engine (coco) log files | + +## Development + +```sh +bun install +bun run lint # tsc --noEmit +bun test +bun run build # bundles dist/index.js and dist/daemon/index.js +``` + +End-to-end smoke test against a running daemon (needs a funded client API key): + +```sh +ROUTSTRD_API_KEY= bun run smoke [model-id ...] +``` diff --git a/COCO-2.0.0-MIGRATION-PLAN.md b/docs/plans/coco-2.0.0-migration.md similarity index 98% rename from COCO-2.0.0-MIGRATION-PLAN.md rename to docs/plans/coco-2.0.0-migration.md index b4e277b..f8f4f71 100644 --- a/COCO-2.0.0-MIGRATION-PLAN.md +++ b/docs/plans/coco-2.0.0-migration.md @@ -1,5 +1,11 @@ # Coco 2.0.0 migration plan +> **Status:** not started as of routstrd v0.4.9. +> Verified against `package.json`: still on `@cashu/coco-core ^1.0.1`, +> `@cashu/coco-sqlite-bun ^1.0.1`, `coco-cashu-core 1.1.2-rc.50`, +> `coco-cashu-plugin-npc 2.4.1`. The NPC compatibility gate in section 1 is +> unresolved. + ## Objectives The migration must accomplish four things safely: diff --git a/package.json b/package.json index 2c9d130..45a8acb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "monitor": "bun src/index.ts monitor", "lint": "tsc --noEmit", "test": "bun test", + "smoke": "scripts/smoke/chat-completions.sh", "build": "bun build src/index.ts --target=bun --outfile=dist/index.js --external better-sqlite3 && bun build src/daemon/index.ts --target=bun --outfile=dist/daemon/index.js --external better-sqlite3", "build:binary": "bun build --compile --no-compile-autoload-dotenv --no-compile-autoload-bunfig src/index.ts --outfile=dist/routstrd", "prepublishOnly": "bun run build" diff --git a/refund.js b/refund.js deleted file mode 100644 index 22d549f..0000000 --- a/refund.js +++ /dev/null @@ -1,33 +0,0 @@ -import crypto from "crypto"; - -const token = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGIBhc3hAMDJkZmYwZjUzZGYyYmM1NTJmNmVlZjdmMzhmOWMzZTU5MTY4MGY5ODM5M2I0MjA1YWQ0ZDA5YzlmMTUyNmVkZGFjWCED0Ln-y1Sx8ukCNDoKK4zepISUXAbIMnAj1yuyJGj3hVFhZKNhZVggk4ymtXIrLMRJf2bV7BOo5wmGfPeH1GlYRnG-mPKg-5phc1ggXf-FYynNa0xYgjub6aGIr_mFM-OEAwyx5VlXtWw7NXlhclggM9X2fnOOW8M-cL5Yvn_iAcb7uAcWremL2ZBZi1-TEu2kYWEBYXN4QGQxODY3MTQ4YWJlYTRhMmI5ODIzZDYyNmQ5ZWIwMmJiM2FiYmJlMjNiMzg0Y2U5M2U4N2MyMjAxOTU3YWNiZDVhY1ghA0S4fM1z2KiJKBmGk6qHVvGwnC6t78dcxX0A8Loy8_72YWSjYWVYIKVwButD6sTWCuMrdLGKj0SSdkO_Jl_sP6bAmnL65otzYXNYINBF5s_e9_WmXrI7GZ3n1KliaZk5Q9i4xN2N1JBUlVEdYXJYIGCJVxKUoIUDdtXLz7HFQg7nrBRIb3DJXt6eiveL03EqpGFhAmFzeEAxNTU3NmE4YTQ5Y2RhMzhhZjQ4N2YxYjhlYzk4ZWYwM2Q0MmZlZWY4NThjYzI3YWJlYjlmYmNlNzhlZTBlZWUxYWNYIQKLaUbivnGndI-kvs_u8KvUH0Sd3Mn7fP1iF2MfQPHgvWFko2FlWCCCCI_auq8R_eFrumwLYRHD_Nab7qe4_Ok8hTmGjkQ_FWFzWCANCP4IAbAw38HJktVCeZMScqYR-BMq-1VxIYRLbI-mXmFyWCDe3L3omLIxu02vqDYsQkIjIoy5HVwV5nz-bmB4F32VzaRhYRhAYXN4QDY1ODk1ZjViYTJkOTA4MjJmMzhmZjkyNWJlZDk4Y2Q1ZTM5NDgxNGJjZGI2MDA1ODUyMThlM2EzY2Y0MDM4ZWJhY1ghA0orpNWfYQihhMLqFuqYVdiCRnvzUs0R2LNbmxK0uivLYWSjYWVYIEKL2qFpFy00hsymiyfIpaKoRTbKexGHVIrO4MlF3FjzYXNYILzd6WqS2IDZrmPi2fFC3_w-LQc6aH0Z_-gr_PUL4vtpYXJYIATNPgMmsbuhLQYaWG-c9Fk0QmHrmxZap5SChukvmbM6pGFhEGFzeEA0MWE5OGY2NTlkM2E5YzgwNjIyN2I0Njg0ODljMTAzOGQxNzE5ZGI5ZTIxOTYwMWRkYmQ3ZGRkNTk1YmQ1ZjkwYWNYIQPMufMoE6Of8fyD2MEhPd3Y3KeKhWzepqOZXKYMQewQeGFko2FlWCCCdcrtZzqfHlXkF4Cm_23dztbXX92YiT8qmotuAxoRt2FzWCBTwbmVsAmQraHkJqsrobFhD3WZ8Uc8g8VBMKHlQBwHGmFyWCDOIIFn71ydyJGOeBuxrWc87hCWV1w_6KxzCLhLFIznDQ"; -const token2 = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCEpGFhGIBhc3hAMWE2YTEzZWFmMWY5Y2FiZDdjYzJhZDZhOTM0MmU4ZTE1MzJhMzljYmFhMjRiYmEzMTVmYzA0ZDM1ZGY5YzVjZmFjWCEDxc6EeUBD1ykxGkr5se8B6M0KIaoanysabCXFM3kTsTlhZKNhZVgggsoE5igqfQrqxxpuA4h7IOlu58rTPd5fgSDbt0PsvJ5hc1ggN1c8DaDHAKP7JeskTBCKMYod8Hbc1UASWMb1PdNYiithclggiFuUJSZ4xQKXpUSF3i56M3wMNfbAOoime-NEAJNvbzukYWEYQGFzeEBkZmNhNDJmNDYyMDcyOTFmNGRjNjY5NzE3MzVmMWY1NmRhNmVhODg3OGZhYTZjMzJkOGZhODEyMWI3YzU5NWE5YWNYIQPvTgwE_lEc4y5AvoR4afrvVeBg8aFaZbX-J2pi1ODQD2Fko2FlWCCKHJQPvkqTBzXSlGi7IohfC7dmZYKnJstkrh090EAAJmFzWCCL6_TLTYOtlPtb7E5zpJrlAwmVoaEeasDbWFdHq8bts2FyWCAkwP2KY06reSkqAre6S0eNp6tH6MOamrBYnMIdkTKCD6RhYRBhc3hAOTYwOTBhOWRhNDM0NzgwM2YyODMyM2MyZTQwNTJmMzE4MjBmMGVmZGMzMTBkMzYwNzZkYzc1NzU1MDRkNzgyZGFjWCECLAnqF1xnlwRHKkgcEpl1w73wXSco_TrqvHzvADRur-NhZKNhZVggs79rTYHAsMXduTS8VRFr17wY8BeS76I2w0plRSzGBdNhc1ggn5YnBEqQhPH7WbiQOIVmVoUL6DuOjXghnH6jsqEiVJphclggl5PftuvVrNaO-FaqHB_iALFFtAlccnG87PIWaTxW7NqkYWECYXN4QDcwNjQ5NWU4YWJkMDIyNjY2ZTc5ZjkwNWU2ZTk5NzU2MmVkZmJkZDU1NDA5NDMwMjNlYjA2ZjU5NDAzNjE2YTlhY1ghA_5PsdXzk5DXHulsXP6n9IadnIYcgqBiKo2ssr7APBFsYWSjYWVYIBj6uJBqZ5tJTqxDgPqhrBCJH3WsU8W73LZITtF_MhM8YXNYIDOLWwQWZ6S8y6Tr3IkuP7rfUSbjw2yP1ut5PNIif6KhYXJYIE-CACyXELfybso_kRN4w1uNKSJO7tc4Ke560ZnbLgFq"; -const token3 = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGIBhc3hAOGNhMTRlMjc1ZGM3ZTU2ZjgzMmNiMWQzMzg3MWQ1NTA0ZmY4ZDdjNDExYTlmMWIzZmY4NDliMDVlMWYwYjdkZWFjWCECqkWTlYeBbMjf9Gu9EvXuwMZSwahjlLdtoKYVUjGvMGphZKNhZVggZAlbzaB-f1W9V6DCXx9-UBR3Wrx6q5YPkdPzqdl2NLhhc1gg0iX6AuofKqUpxtYzTS8RK7dDSOhNeuqo4d-OAyEOY7thclggZJpwNkrTQmDQdvPgPuMz1UZoxJB2ntuAB_BJ3B8V_-ykYWEIYXN4QDAwZGUzNjc0OWI5OTMxNzUzODIwMWRiN2FmYmFkZGQyYmVkYWIwYzE4MGQ3YTk4YmQwMTU5ODhhNmU2NzNkN2ZhY1ghArRuuYHPk0zZDarPLFVKXKsWJdrOoVrX2cGOmJHlzj1fYWSjYWVYIA3tdTlmP3AWgUm67_8OGecU__-Bznp_5Z4gxdog4j-BYXNYIC4v6A0OeJ1ftOGaJ5xyDaRoeUcvxZC50k1zmptwca2uYXJYIKwoi2amxdoo6DmakRNC9o1BNBbcVLRovVLTZ_HWrwz6pGFhAmFzeEBlZTJjMDdjNDIyNWY1ZDc5NjUxOGEyN2VkYmVhMjVlNWNiODNlMTYyOTBlYzQ1MTA3MmZhMDMyNDZlZjMzYmZjYWNYIQLnKi-IHFdCqOzKcejPC325v_q7DDBWG4kGbEeEfYj1wmFko2FlWCDJFzWJNhHqQVocBT34rokTvd5MNc2l0yd-22mlrA_J42FzWCCnrH5rsQBu8M5xzz0jdh6iDw798PKXwIjjThsBrJmJ5GFyWCAP5YPIf8NRVQACOX8qTmCFJLly6MZdrPpLKW9crxX3TKRhYRhAYXN4QGU1YTVkODczY2JiN2ZiYjkwZjBhNWJhMjcwODZlNWUwNzczYjhhZmM2MjkxYjkyZDZjNmRjNWUyOWIwNjBmYThhY1ghAsOHo3BVNDkz9aexnLCBGC7fbjAt9qrkMLd5MFSwlkxDYWSjYWVYILVffHeqv2Wj3XBCAvVs5XMizFyO6QXKJX-WkgYejDbhYXNYIHwpn3nGOU8qIU05mvUoLjsQPC50iSp-m0pQc5h_TojGYXJYIFqpEyKa6zmx-es7MGHrBjS34unW7gHirPiWBtNhOYPjpGFhCGFzeEBjZTdjNWQwYTdhMTA1YjFhMGFlODdmNjQ5YjgxY2Y5MDUzNDBkZDg2ZWRmY2EyZDg5ZGI3ZmIwMGU3ZTEwNmFjYWNYIQKHOiRDX17csadc2W9ApXSC9z9OA9Ow_B_0Z3t-LjXlVWFko2FlWCA_BVVJUfd_8aqq6wyc2hna274nMlFJVcPUap8jfLHdZ2FzWCBf7TMpb7gy4H5p2JU4QU9aftsngHLoNRT03e-9IxpdtWFyWCAONKjqsMSZODlyVgbtHJH2Hs3R25mlK3tbM-GsR5KGJQ"; -const token4 = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCFpGFhGEBhc3hAM2JiY2M1OTk4MzA3YWQ2MDcyMTUxYzgyMmQ0N2ZmZGMzYTA4MGQ3MDIwZmM2NGM5MGY0NWYyNDA3ZDYwY2Y3Y2FjWCED1kLTzhP_9Z-emLn9p6ypf5WJnu37aXntJiLGczkuJIhhZKNhZVgg0gm2HX3PRE1e73hD53ETXD9K2sa7g74KME6OPHFyQ2xhc1ggD9yp5h77Lw5iVSeEM0pnvx3iOYi2KZ7USzzupHZkUNRhclggp5x96k0_bpDlYzUpsSS_YejP5xdbRKppx2ryUFkJQzakYWEBYXN4QGY5YzFmMjlkM2Q1MzgxODRjYmViNDA5Yjg4ZTNjM2MzNjU5M2I4NDVjMTA1N2RkNGI4MjY3N2M4NzQ1OWRiZjNhY1ghAzUJP4iEl3rzII-lBI6SD5BgsNPPIyb0otqvCjjeRMEKYWSjYWVYIBB_v8-hl5kgLCUuetZyS8bKYR0qDct9FatrHmktjtIPYXNYIDpeyARyUcMX8VI-aitlYYRrDZwnI0i2JK32h6vW7lNmYXJYIIw64U0Fnur7GKglqxmZLhqGwilg7EEvw7CF59ZpPrXepGFhGIBhc3hAOGZmOGYwODA1Zjg2NWFlMDUzNzY3NGZmMmQ0MTk2YjczNWViZWVkMDYxY2Q4NmQ5MmFkOTVkNjIxNGIyNmFkOWFjWCECJSfVy1j8HGKlIW8WPjaL7laDkh8S74bHgpo827L_yw5hZKNhZVggg24YJokE1Xz_g8rv1Lvs_YtoEHpbNNHnIE1pjJrYYo1hc1ggz5P19DDGdG3V_zNOdMoeBe-OlkhBcIiFFQ1h0ayfatphclgg6__UEWXTYs5oI4_VPh16pa6AYuqNf9IQjT_a4WDK8yKkYWEQYXN4QGMxNmMzM2ZjZTI4YmZhNzc5OWUyYmMwNDA3MzVkNmQzZmFlMTUyYjZkZTE0OTZhNjU5NzlmZjBmYzkwOWJjZDBhY1ghApQQGNGslVLEqkkT-lDnRo2aYqyXoT-_McAz4oS4sa-WYWSjYWVYIHFK50vYiJblhpmrkxfN-p0pyylK2dEinSuobMdJt8rRYXNYIBrTahdM6XVoJ02F9G7L9sLxdDX-I15k9rysVA33bF7iYXJYIIkE3phWIy8O1F-_7uiw8MbyVSvzmnipHdMV572pynoPpGFhAWFzeEA1NGNjM2UyODM1MGM4YzIzNjdlZjFkZjg0NGQ3NzRhMjM0NzYxZTA1OTJkNjg2OTRiMGQzOGMyZWU5MTEzNWQ3YWNYIQNmdhuY0oNhnb4QMBKtyP3QXXpUkmBDD9zTetSgK9op0mFko2FlWCCh5r-bIQFTdyFtpOU6YZoJW3cMDf3ruAdNgElSUPm9JmFzWCCiIT_E7W1gJjuATLUJVd7t1qhkPX94SJRd1f2kmvF1uGFyWCAr4psTvW75ntrNFghNvw1VBJ-Qt6elhDCSGpRcLXNN_Q"; -const token5 = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCEpGFhGIBhc3hAMzgxMTViZWY0NzA3MjNmZjczZGJhOTk5YWY2M2MwZDlhMmViMzQ3OTA1MWUzMjM3NjQ4YWFjYTVkMWFhN2Y1MGFjWCECw0MPnt_M_lwq5ZkmMvgtJHVa2A8PYTtbM6-C0do3NF5hZKNhZVggONdGnCnTzAOKKL9v1CnruOayiVpWDwGV9zRRHhh6KX9hc1ggSdj6FOCa1-ub6eI8hF42RbdMonb0-CYwRoWm0fSH7_phclggeQXZy7n8e7RzL1rjqwhYDjcqrUxj1v3aPAM_4cEaauqkYWEYQGFzeEA5NTg5YTFkODEyOTg3MzE0ZDFiYTE5ZDdjZmM1MzZlMTlmNGVkNGVlNTg3MDA4NjRmNjg4NjA0MjI2Mjk5YzFlYWNYIQNWML_D2vtlMYtimuEgIkGNYvaFZFWjyWwkZdyrT2ijeGFko2FlWCD1N0B9J5B91SxMhTg1PhWytUjWJPFOWvlm1UpXJ7UhqWFzWCASNE9dGijBP9jHIgyjnHnHI0G8a7qWsPd8Uhc0PwxS4mFyWCA3vukTqXvkF0Yit9w8wkyPWTIYLxHefjSHzaOuyxI6XaRhYRBhc3hANWJhOWM1MTA3ZTc1MWM4MGE2NzBiMjI2MTUxYjMyZDIzNzNiZjU1M2EwM2VlNTk2ZWQ1NTEyOTFiY2UwMWZmMWFjWCEC6f7l5IXXSaP9eRZmdUYs9liU2dVxNTaNPgPlPgnmzs1hZKNhZVggQaxRZNwg_cPymPLpgWLetqAMuUd5kRfMjNID22snM5dhc1gg0CDW54cjN5HF3FIogQAB3TqowQiH5aJ5ZbHIIKbX59Fhclggolu8liKdOYxCrwmU4p7VsPAflDJecMiUePmPLRmUwS6kYWECYXN4QDY5MDRjM2RkZmJlNjRmOGRmY2NkOWI0NWFmNWEyZTMzYzcyMzIzZTU5N2VkMGNiM2NmZTJhMWIxYzRmOGE4YjZhY1ghA5wv4H1I4NBMUy9qQWavrG6AW3pduPm-F7JUxYt0KC9kYWSjYWVYIIthil2Aj1kiIVmRlziK9NjEI_Dk1JKDn0pazj4Z8qVQYXNYIMNyB7-TbSR_9gNyO_YKohy3D79KhcsbAxV6MLi_RAnEYXJYIM4g3a130UC94KjWYufe-DWVGAQsGNTgd2fLT-DZlrS9"; - -// Hash the token with SHA256 -const hash = crypto.createHash("sha256").update(token).digest("hex"); -console.log("SHA256 hash:", hash); - -// Make the POST request -const url = `https://llm.satsandsports.cash/v1/cashu-refund/${hash}`; - -fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, -}) - .then((res) => res.json()) - .then((data) => { - console.log("Response:", JSON.stringify(data, null, 2)); - }) - .catch((err) => { - console.error("Error:", err.message); - }); diff --git a/refund_new.js b/refund_new.js deleted file mode 100644 index fcc6921..0000000 --- a/refund_new.js +++ /dev/null @@ -1,20 +0,0 @@ -const token = - "cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSAAQeTfbDMhlYXCGpGFhGIBhc3hAYzNmMDFjNDYxYjMxNmEwM2E2NGMzNjBmZDJhNjk1OThmMzE2MmM4NGMzZjlmZWExYWYxYTNjN2EyNWY3OTdmNGFjWCEC8iVrY11X2Hs_cYpWG7iD8Xt4DPcIZbVO0RAYoemP7udhZKNhZVggD1RQud6FdadMsbgJ_JB6h-qsJX0qLY83AIig3ocSZdNhc1ggOJIqxJpCn7dIqUO4lLLQE4VMs4TDu-bXWr0Kb3W8cYNhclgg_ZB2BEjqNIemFZ3GGGt-ZROgKNntpcyLX6eAJMYM6p2kYWECYXN4QGRlZjdhMjc5ZmNlZjY2ZmY5YzMyZDczODljYmJmNjUyNGIzY2M0OTQzYmM3MWJkNjA2ZDZiNzdhMjIzODIyMzNhY1ghA591WOMW3zhSXMajVUsgcXzoVReBsnxvq_xuy0gmRSI_YWSjYWVYIASUNdNthr7DNLBsCuIMisKt_KAUNXKVbcvHuUXVrxWCYXNYIBB5rsypISo_LcFKnaqEqbdEBi1SpWRgRLZfbFxqAo1IYXJYIMp7sk2t2k9vh1ZmhnILSlfo2245EqEvjgw8BryNY8QapGFhCGFzeEBhMWY2MzgwYjA2ZmE2NDhlMTY1NmQ5OWNmMThiYWNiYTdhZDVlMmNmNTY1MDAxMTc3NWQ3YjJkNTg5ODkwNzI1YWNYIQKSoj8-ipFq_s6Jes9APqaC5sNUz1icoZ0R6B3HCWoQ3GFko2FlWCAoiS9cHLbzHD9RWU6osZDCfo-czzEidp-pea9ejCEeSGFzWCA_YnzFgPvmXJX65r4QUJQICBcThdC7ZvdZlKqba598pmFyWCBHEneehhb0PxiFrwr6DgSZXWmWKft9WJwXlxr05HHmWqRhYRhAYXN4QDQ3NDkxMWZmYTk4ZTAzYTBkYWM5ODdjMjBjZWI5ZjlkMGE1MGNkNDM1ZjkwMDBhZWFhMjRkMGJlODgzMGM4ZjFhY1ghA0V01nVSIT46umSZL1XABGFGT4nAPnSYEOHUK5eT5xa-YWSjYWVYIHLOM7_1xe30zcNFFQEXgijF8joTo6yFLFDovHXWCr-sYXNYIILMBt2l053_K8HRRj5P-OWrVhg3oUF9NRYyzFQixEW3YXJYIIOu8UCudw8LMvm87O9X6A2VOS_AtDbXNGHEe2krTPTmpGFhCGFzeEA1N2I0OGNlNTNjMDI2MDM4ZmFlMTcxZjYzM2M5NzYzY2U3ZTA0M2E5MTk0N2VhZDM0Y2JhZDQwZGNmMTI5NmMwYWNYIQJ8KXhq9MuwGEw3xf4CFVpSuLmlMHohE4DByPpeW9NYqWFko2FlWCCHLILey0LgSBMe79A0f8gc4vzoI2AZ5vv53jWH9GUcFmFzWCBB7PcuhuKazJYtRXKMWtmQbRv66qkgdt3I5qetd7DfNGFyWCDBj4R1dWGOulvmQxsbpVJwAAM1RkOdSlgztd1ZOUxHfKRhYQRhc3hAMDJjZWQ4NzZjNGU1MGRjYmNmNDViN2U2MGZjNGRiYThiNzMzNzRkMDU2MWFiMTc0Nzc2ZDNjMmE5MjliOWU0NWFjWCED4OBpEfpelcMak8IAzXO8xRjon5bj5VC3dMjRF1js-gJhZKNhZVggpN8QqMUwG52q-t7xrwkDE6lS8Cw67-_68ERu0X2QuAphc1ggrYyoFeGIT73AT-3gd9tCK_1NYh84s9f63oBytxKTv2Vhclggvi2atyfhijg7QLbdEkNQkrHcW0ocJElhPrNmND9_so8"; - -// Make the POST request -const url = `https://routstr.otrta.me/v1/balance/refund`; - -fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Cashu": `${token}`, - }, -}) - .then((res) => res.json()) - .then((data) => { - console.log("Response:", JSON.stringify(data, null, 2)); - }) - .catch((err) => { - console.error("Error:", err.message); - }); diff --git a/routstr-cost-logging.md b/routstr-cost-logging.md deleted file mode 100644 index 11847f0..0000000 --- a/routstr-cost-logging.md +++ /dev/null @@ -1,71 +0,0 @@ -# routstr Proxy Cost Logging Issue - -## Problem - -The routstr proxy at `localhost:8009` returns usage data that differs slightly from what the pi-ai `openai-completions` provider expects. - -## What routstr Returns - -**Streaming response (last chunk):** -```json -{ - "usage": { - "prompt_tokens": 9, - "completion_tokens": 9, - "total_tokens": 18, - "cost": 0.000018, - "prompt_tokens_details": { - "cached_tokens": 0, - "cache_write_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "cost": { - "total_usd": 0.000018 - } -} -``` - -## What pi-ai Expects - -The `openai-completions` provider in pi-ai expects the standard OpenAI format: - -```json -{ - "usage": { - "prompt_tokens": 9, - "completion_tokens": 9, - "prompt_tokens_details": { - "cached_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - } -} -``` - -## Current Handling - -The pi-ai provider already handles the standard format correctly via `parseChunkUsage()`: -- `input` ← `prompt_tokens - cached_tokens` -- `output` ← `completion_tokens + reasoning_tokens` -- `cacheRead` ← `prompt_tokens_details.cached_tokens` -- `cacheWrite` ← **NOT CURRENTLY PARSED** (hardcoded to 0) - -The provider then calculates cost using `calculateCost()` based on the model's configured cost per million tokens, ignoring any `cost` field from the response. - -## Gap - -The `parseChunkUsage()` function in `packages/ai/src/providers/openai-completions.ts` does not currently extract: -1. `cache_write_tokens` from `prompt_tokens_details` (routstr-specific field) - -Currently `cacheWrite` is hardcoded to 0. - -## Resolution - -The existing pi-ai `openai-completions` provider should work with routstr as-is since routstr returns the standard OpenAI format fields. The usage should be logged correctly if: -1. `stream_options: { include_usage: true }` is passed -2. The model has a `cost` configuration in the registry diff --git a/src/TUI refactor.md b/src/TUI refactor.md deleted file mode 100644 index ff7547a..0000000 --- a/src/TUI refactor.md +++ /dev/null @@ -1,113 +0,0 @@ -# TUI refactor plan - -## Goals -- Move the usage TUI implementation out of `src/cli/usage-tui.ts` into a dedicated `src/tui/` folder. -- Reduce the size and responsibility of the current monolithic file. -- Keep the existing CLI entrypoint stable so current usage does not break. -- Preserve behavior while making future TUI work easier. - -## Current state -`src/cli/usage-tui.ts` currently mixes several concerns in one file: -- TUI-specific types and constants -- ANSI/terminal helpers -- scroll/search/vim navigation state -- data fetching from the daemon -- usage aggregation/stat helpers -- rendering for all tabs -- app lifecycle and keyboard event handling - -This makes the file hard to extend safely. - -## Refactor strategy -Do this incrementally and keep a thin compatibility wrapper in `src/cli/usage-tui.ts`. - -### Target structure -- `src/tui/usage/index.ts` - - public entrypoint: `runUsageTui()` -- `src/tui/usage/types.ts` - - `UsageStats`, tab ids, tab metadata, derived stat types -- `src/tui/usage/constants.ts` - - tabs, colors, model/client color maps -- `src/tui/usage/terminal.ts` - - ANSI helpers, width/height helpers, `stripAnsi` -- `src/tui/usage/state.ts` - - vim/search/scroll state and state mutation helpers -- `src/tui/usage/data.ts` - - `fetchUsage()` and usage aggregation helpers -- `src/tui/usage/render.ts` - - shared render helpers and tab renderers -- `src/tui/usage/app.ts` - - main loop, render orchestration, input handling, cleanup -- `src/cli/usage-tui.ts` - - compatibility wrapper that re-exports or calls `runUsageTui()` from `src/tui/usage` - -## Design choices -### 1. Keep CLI path compatibility -Do not delete the CLI file outright. Turn it into a tiny wrapper: -- minimal import from `../tui/usage/index.ts` -- export `runUsageTui()` - -This avoids breaking any existing imports or scripts. - -### 2. Separate pure logic from side effects -Keep these pure where possible: -- aggregation helpers -- formatting helpers -- render helpers that return strings -- scroll clamping logic - -Keep side effects isolated in the app layer: -- reading terminal size -- writing to stdout -- raw mode setup -- signal handling -- interval scheduling - -### 3. Avoid over-engineering -This should be a pragmatic refactor, not a framework: -- no unnecessary classes -- keep function-based design -- only extract modules around clear responsibility boundaries - -### 4. Preserve behavior first -No UX changes unless needed to support the extraction. -That means: -- same tabs -- same keybindings -- same output format -- same fetch cadence -- same search/scroll behavior - -## Implementation steps -1. Create `src/tui/usage/`. -2. Extract types/constants first. -3. Extract terminal helpers. -4. Extract data fetching + aggregation helpers. -5. Extract state/search/scroll logic. -6. Extract rendering helpers + tab renderers. -7. Build `app.ts` using the extracted modules. -8. Replace `src/cli/usage-tui.ts` with a thin wrapper. -9. Run a TypeScript/bun check and fix imports. -10. Smoke-test keyboard handling and rendering behavior. - -## Risks -- circular imports between render/state/constants -- broken relative import paths during extraction -- subtle behavior regressions in scroll/search state -- terminal escape handling differences if helpers are split carelessly - -## Validation checklist -- `src/cli/usage-tui.ts` still exposes `runUsageTui()` -- TUI starts from the same CLI path -- scroll still works for long content -- vim keys still work -- arrow keys still work -- tab switching still resets scroll -- search mode still works -- cleanup still restores cursor and alternate screen - -## Non-goals -- redesigning the UI -- changing tab contents -- introducing tests unless needed for safety -- adding new features unrelated to the refactor diff --git a/src/tui/usage/render.test.ts b/src/tui/usage/render.test.ts new file mode 100644 index 0000000..1a949aa --- /dev/null +++ b/src/tui/usage/render.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "bun:test"; +import { renderBox } from "./render"; +import { stripAnsi } from "./terminal"; + +/** Visible column count of a rendered line, ignoring ANSI colour codes. */ +function visibleWidths(box: string): number[] { + return box.split("\n").map((line) => stripAnsi(line).length); +} + +describe("renderBox", () => { + it("renders every line at the requested width when given a title", () => { + const box = renderBox(["Hello World", "Line 2"], 40, "Title"); + + expect(box.split("\n")).toHaveLength(4); // top, 2 content rows, bottom + for (const width of visibleWidths(box)) { + expect(width).toBe(40); + } + }); + + it("renders every line at the requested width without a title", () => { + const box = renderBox(["Hello World", "Line 2"], 40); + + for (const width of visibleWidths(box)) { + expect(width).toBe(40); + } + }); + + it("puts the title in the top border", () => { + const [topBorder] = renderBox(["body"], 40, "Stats of Sats").split("\n"); + + expect(topBorder).toContain("Stats of Sats"); + }); + + it("pads content rows so ANSI-coloured lines still align", () => { + const colored = "\x1b[32mgreen\x1b[0m"; + const box = renderBox([colored, "plain"], 30); + + for (const width of visibleWidths(box)) { + expect(width).toBe(30); + } + }); + + it("composes two half-width boxes into rows of the full width", () => { + const width = 80; + const halfWidth1 = Math.floor(width / 2); + const halfWidth2 = width - halfWidth1; + + const left = renderBox( + ["Total Spent: 12.78k sats", "Total Requests: 1.0k"], + halfWidth1, + "Stats of Sats", + ).split("\n"); + const right = renderBox( + ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"], + halfWidth2, + "Token Stats", + ).split("\n"); + + expect(left).toHaveLength(right.length); + for (let i = 0; i < left.length; i++) { + expect(stripAnsi(left[i]! + right[i]!).length).toBe(width); + } + }); + + it("pads the shorter side when the two boxes have unequal heights", () => { + const width = 80; + const halfWidth1 = Math.floor(width / 2); + const halfWidth2 = width - halfWidth1; + + const left = renderBox(["a", "b", "c"], halfWidth1, "Left").split("\n"); + const right = renderBox(["x"], halfWidth2, "Right").split("\n"); + + const rows = Math.max(left.length, right.length); + for (let i = 0; i < rows; i++) { + const l = left[i] ?? " ".repeat(halfWidth1); + const r = right[i] ?? " ".repeat(halfWidth2); + expect(stripAnsi(l + r).length).toBe(width); + } + }); +}); diff --git a/test_box.ts b/test_box.ts deleted file mode 100644 index 0617cf2..0000000 --- a/test_box.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { renderBox } from "./src/tui/usage/render.ts"; -import { stripAnsi } from "./src/tui/usage/terminal.ts"; - -const w = 40; -const testBox = renderBox(["Hello World", "Line 2"], w, "Title"); -console.log(testBox); -const lines = testBox.split("\n"); -lines.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`)); - -console.log("---"); - -const testBoxNoTitle = renderBox(["Hello World", "Line 2"], w); -console.log(testBoxNoTitle); -const lines2 = testBoxNoTitle.split("\n"); -lines2.forEach((l, i) => console.log(`Line ${i} length: ${stripAnsi(l).length}`)); diff --git a/test_split_box.ts b/test_split_box.ts deleted file mode 100644 index 538a7cc..0000000 --- a/test_split_box.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { renderBox } from "./src/tui/usage/render.ts"; - -const width = 80; -const halfWidth1 = Math.floor(width / 2); -const halfWidth2 = width - halfWidth1; - -const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"]; -const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"]; - -const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats"); -const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats"); - -const leftLines = leftBoxStr.split("\n"); -const rightLines = rightBoxStr.split("\n"); - -const combinedContent = leftLines.map((l, i) => l + (rightLines[i] || " ".repeat(halfWidth2))).join("\n"); -console.log(combinedContent); diff --git a/test_split_box2.ts b/test_split_box2.ts deleted file mode 100644 index 9b08c14..0000000 --- a/test_split_box2.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { renderBox } from "./src/tui/usage/render.ts"; - -const width = 80; -const halfWidth1 = Math.floor(width / 2); -const halfWidth2 = width - halfWidth1; - -const leftBox = ["Total Spent: 12.78k sats", "Total Requests: 1.0k"]; -const rightBox = ["Total Tokens: 25.8M", "Avg Tokens/Req: 25.8K"]; - -const leftBoxStr = renderBox(leftBox, halfWidth1, "Stats of Sats"); -const rightBoxStr = renderBox(rightBox, halfWidth2, "Token Stats"); - -const leftLines = leftBoxStr.split("\n"); -const rightLines = rightBoxStr.split("\n"); - -const maxLines = Math.max(leftLines.length, rightLines.length); -const combinedLines: string[] = []; -for (let i = 0; i < maxLines; i++) { - const l = leftLines[i] || " ".repeat(Math.floor(width / 2)); - const r = rightLines[i] || " ".repeat(Math.ceil(width / 2)); - combinedLines.push(l + r); -} -console.log(combinedLines.join("\n")); diff --git a/v1-messages-format-report.md b/v1-messages-format-report.md deleted file mode 100644 index 43c09c0..0000000 --- a/v1-messages-format-report.md +++ /dev/null @@ -1,223 +0,0 @@ -# Report: why `POST /v1/messages` on port 8008 returns chat-completions chunks while port 8009 preserves Messages API format - -Date: 2026-03-28 - -## Summary - -I tested the same request against both local daemons using `scripts/test-direct-local.ts` semantics (`POST /v1/messages`, `stream: true`). - -- `localhost:8009` preserves the Anthropic/OpenAI Messages-style stream as expected: - - `message_start` - - `content_block_start` - - `content_block_delta` - - `message_delta` - - `message_stop` -- `localhost:8008` returns OpenAI chat-completions streaming chunks instead: - - `object: "chat.completion.chunk"` - - `choices[].delta` - -This is **not** because the SDK itself is incapable of preserving `/v1/messages`. -It happens because the two daemons call the SDK differently. - ---- - -## What I tested - -### 8008 (`../routstrd/`) - -Listener: -- `../routstrd/src/daemon/index.ts` -- request handler in `../routstrd/src/daemon/http/index.ts` - -Observed result for `POST http://localhost:8008/v1/messages`: -- response `content-type: text/event-stream` -- SSE payload is OpenAI chat-completions chunks (`chat.completion.chunk`) - -### 8009 (`routstr-chat/scripts/routstr-daemon.ts`) - -Listener: -- `scripts/routstr-daemon.ts` - -Observed result for `POST http://localhost:8009/v1/messages`: -- response `content-type: text/event-stream` -- SSE payload preserves Messages API events (`message_start`, `content_block_delta`, etc.) - ---- - -## Direct cause - -### 8009 forwards the incoming path to the SDK - -In `routstr-chat/scripts/routstr-daemon.ts`, the request is routed with: - -```ts -await routeRequestsToNodeResponse({ - modelId, - requestBody, - path: url.pathname, - headers: forwardedHeaders, - ... -}); -``` - -Because it passes: - -```ts -path: url.pathname -``` - -an incoming request to `/v1/messages` stays `/v1/messages` all the way through the SDK and upstream provider routing. - -### 8008 does **not** forward the incoming path - -In `../routstrd/src/daemon/http/index.ts`, the request is routed with: - -```ts -const response = await routeRequests({ - modelId, - requestBody, - forcedProvider, - headers: incomingHeaders, - walletAdapter: deps.walletAdapter, - storageAdapter: deps.storageAdapter, - providerRegistry: deps.providerRegistry, - discoveryAdapter: deps.discoveryAdapter, - modelManager: deps.modelManager, - debugLevel: "DEBUG", - mode: deps.mode, - usageTrackingDriver: deps.usageTrackingDriver, - sdkStore: deps.store, -}); -``` - -Notice: **no `path` is passed**. - -In the SDK, `routeRequests()` defaults the path to: - -```ts -path = "/v1/chat/completions" -``` - -from: -- `routstr-chat/sdk/routeRequests.ts` - -So even when the client calls: - -```http -POST /v1/messages -``` - -on port 8008, the daemon internally re-routes it as: - -```http -POST /v1/chat/completions -``` - -That is why the upstream/provider response is converted into chat-completions format. - ---- - -## Why the behavior differs even though both are built on the same SDK - -Both daemons use the same SDK primitives, but: - -- **8009** uses `routeRequestsToNodeResponse(...)` and explicitly passes `path: url.pathname` -- **8008** uses `routeRequests(...)` and relies on the SDK default path, which is `/v1/chat/completions` - -So the format difference is caused by **daemon integration code**, not by a provider-specific quirk and not by an unavoidable SDK conversion. - ---- - -## Important implementation detail - -The SDK helper itself documents this default: - -- `routstr-chat/sdk/routeRequests.ts` - -```ts -/** Optional: API path (defaults to /v1/chat/completions) */ -``` - -and in `resolveRouteRequestContext(...)`: - -```ts -path = "/v1/chat/completions" -``` - -Therefore any caller that omits `path` will get chat-completions semantics by default. - ---- - -## Evidence from runtime tests - -### Port 8008 - -Observed streamed chunks included: - -- `object: "chat.completion.chunk"` -- `choices[0].delta.content` -- final `[DONE]` - -### Port 8009 - -Observed streamed events included: - -- `event: message_start` -- `event: content_block_start` -- `event: content_block_delta` -- `event: message_delta` -- `event: message_stop` -- final `[DONE]` - -This matches the code-path difference above. - ---- - -## Recommended fix - -In `../routstrd/src/daemon/http/index.ts`, pass the incoming path through to the SDK: - -```ts -const response = await routeRequests({ - modelId, - requestBody, - path: url.pathname, - forcedProvider, - headers: incomingHeaders, - walletAdapter: deps.walletAdapter, - storageAdapter: deps.storageAdapter, - providerRegistry: deps.providerRegistry, - discoveryAdapter: deps.discoveryAdapter, - modelManager: deps.modelManager, - debugLevel: "DEBUG", - mode: deps.mode, - usageTrackingDriver: deps.usageTrackingDriver, - sdkStore: deps.store, -}); -``` - -This should make `POST /v1/messages` on port 8008 preserve the Messages API format, matching port 8009. - ---- - -## Secondary note - -Port 8008 currently uses `routeRequests(...)` and then manually streams the returned response body to `res`. -Port 8009 uses `routeRequestsToNodeResponse(...)` directly. - -That difference is probably not the root cause here. -The root cause is specifically that **8008 drops the incoming request path and falls back to the SDK default of `/v1/chat/completions`**. - ---- - -## Conclusion - -The reason `localhost:8008/v1/messages` appears to "convert to chat completions" is: - -1. `../routstrd` receives `/v1/messages` -2. its handler calls `routeRequests(...)` without `path` -3. the SDK defaults `path` to `/v1/chat/completions` -4. the upstream request is therefore made against chat completions -5. the stream returned is chat-completions SSE, not Messages API SSE - -Port 8009 works because it explicitly forwards `url.pathname` into the SDK call.