routstrd 0.4.10 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -14
- package/SECURITY.md +0 -1
- package/SKILL.md +242 -52
- package/dist/daemon/index.js +1855 -400
- package/dist/index.js +2441 -908
- package/package.json +3 -2
- package/src/daemon/http/index.ts +5 -11
- package/src/daemon/http/request-body.ts +67 -0
- package/src/daemon/wallet/coco-client.ts +20 -5
- package/src/daemon/wallet/trusted-mints.ts +87 -0
- package/src/integrations/pi.ts +137 -32
- package/src/integrations/registry.ts +14 -0
- package/src/tui/usage/render.ts +161 -22
- package/src/TUI refactor.md +0 -113
package/src/tui/usage/render.ts
CHANGED
|
@@ -585,14 +585,111 @@ function truncateNpub(npub: string): string {
|
|
|
585
585
|
return npub.slice(0, 10) + "…" + npub.slice(-6);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Token breakdown of a single request, split the way the TUI colours it:
|
|
590
|
+
* input read from the prompt cache (green) vs input that was not (red,
|
|
591
|
+
* i.e. cache writes plus uncached input), plus output tokens.
|
|
592
|
+
*/
|
|
593
|
+
export interface TokenSegments {
|
|
594
|
+
/** Input tokens served from the prompt cache. */
|
|
595
|
+
cacheRead: number;
|
|
596
|
+
/** Input tokens that were not served from the cache. */
|
|
597
|
+
notCached: number;
|
|
598
|
+
/** Total input tokens, cached and not. */
|
|
599
|
+
input: number;
|
|
600
|
+
output: number;
|
|
601
|
+
/** Input + output. */
|
|
602
|
+
total: number;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Split a usage entry into cache read / not-cached input and output tokens.
|
|
607
|
+
*
|
|
608
|
+
* Providers report prompt tokens inconsistently: OpenAI-style `prompt_tokens`
|
|
609
|
+
* already includes the cached subsets, while Anthropic-style `input_tokens`
|
|
610
|
+
* reports them alongside. If the prompt is at least as large as the cached
|
|
611
|
+
* counts it is treated as containing them (OpenAI); otherwise the cached
|
|
612
|
+
* counts are extra (Anthropic). `input` is the sum of what is drawn rather
|
|
613
|
+
* than a possibly under-counted `totalTokens`.
|
|
614
|
+
*/
|
|
615
|
+
export function tokenSegments(entry: {
|
|
616
|
+
promptTokens?: number;
|
|
617
|
+
completionTokens?: number;
|
|
618
|
+
cacheReadInputTokens?: number;
|
|
619
|
+
cacheCreationInputTokens?: number;
|
|
620
|
+
}): TokenSegments {
|
|
621
|
+
const cacheRead = Math.max(0, entry.cacheReadInputTokens || 0);
|
|
622
|
+
const cacheWrite = Math.max(0, entry.cacheCreationInputTokens || 0);
|
|
623
|
+
const prompt = Math.max(0, entry.promptTokens || 0);
|
|
624
|
+
const output = Math.max(0, entry.completionTokens || 0);
|
|
625
|
+
const cached = cacheRead + cacheWrite;
|
|
626
|
+
const uncachedInput = prompt >= cached ? prompt - cached : prompt;
|
|
627
|
+
const notCached = cacheWrite + uncachedInput;
|
|
628
|
+
const input = cacheRead + notCached;
|
|
629
|
+
return { cacheRead, notCached, input, output, total: input + output };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** One coloured slice of a {@link renderStackedBar}. */
|
|
633
|
+
export interface BarSegment {
|
|
634
|
+
/** Segment magnitude; negatives are treated as zero. */
|
|
635
|
+
value: number;
|
|
636
|
+
/** ANSI escape applied to this segment's cells. */
|
|
637
|
+
color: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Render a fixed-width stacked bar: `trackWidth` cells split between the
|
|
642
|
+
* segments in proportion to their values, so the bar conveys *composition*
|
|
643
|
+
* (magnitude is reported separately, e.g. as the token total beside it).
|
|
644
|
+
*
|
|
645
|
+
* Cell counts use largest-remainder rounding, so the segments always fill
|
|
646
|
+
* exactly `trackWidth` cells. A row with no tokens renders as an empty track.
|
|
647
|
+
*/
|
|
648
|
+
export function renderStackedBar(segments: BarSegment[], trackWidth: number): string {
|
|
649
|
+
const track = Math.max(0, Math.floor(trackWidth));
|
|
650
|
+
if (track === 0) return "";
|
|
651
|
+
|
|
652
|
+
const values = segments.map((segment) => Math.max(0, segment.value));
|
|
653
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
654
|
+
if (total <= 0) return " ".repeat(track);
|
|
655
|
+
|
|
656
|
+
const exact = values.map((value) => (value / total) * track);
|
|
657
|
+
const cells = exact.map((value) => Math.floor(value));
|
|
658
|
+
let remaining = track - cells.reduce((sum, value) => sum + value, 0);
|
|
659
|
+
|
|
660
|
+
// Largest-remainder: leftover cells go to the biggest fractional parts.
|
|
661
|
+
const remainders = exact
|
|
662
|
+
.map((value, index) => ({ index, frac: value - Math.floor(value) }))
|
|
663
|
+
.sort((a, b) => b.frac - a.frac);
|
|
664
|
+
for (let i = 0; i < remainders.length && remaining > 0; i++, remaining--) {
|
|
665
|
+
cells[remainders[i]!.index]! += 1;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const bar = segments
|
|
669
|
+
.map((segment, i) => (cells[i]! > 0 ? `${segment.color}${"█".repeat(cells[i]!)}` : ""))
|
|
670
|
+
.join("");
|
|
671
|
+
return bar.length > 0 ? bar + COLORS.reset : " ".repeat(track);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Colour coding for the Recent tab's cache-hit bars. */
|
|
675
|
+
const TOKEN_BAR_COLORS = {
|
|
676
|
+
cacheRead: COLORS.green,
|
|
677
|
+
notCached: COLORS.red,
|
|
678
|
+
};
|
|
679
|
+
|
|
588
680
|
export function renderRecent(stats: UsageStats, width: number, naming: ClientNaming): string {
|
|
589
681
|
const recentEntries = stats.entries.slice(0, 50);
|
|
590
682
|
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
|
591
683
|
|
|
592
|
-
const timeCol =
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
684
|
+
const timeCol = 8;
|
|
685
|
+
const costCol = 12;
|
|
686
|
+
// Width reserved left of the token counts for the cache-hit bar. `CACHE HIT`
|
|
687
|
+
// is the header drawn over the bar, so the bar can never be narrower.
|
|
688
|
+
const minBarWidth = "CACHE HIT".length;
|
|
689
|
+
const maxBarWidth = 20;
|
|
690
|
+
const minProviderCol = 12;
|
|
691
|
+
const minModelCol = 10;
|
|
692
|
+
const minClientCol = 6;
|
|
596
693
|
|
|
597
694
|
// Remote mode: `Alice (claude-code)` — owner display name + the client id
|
|
598
695
|
// with the `-<npub tail>` suffix stripped. Local mode (no owner/name data)
|
|
@@ -602,39 +699,81 @@ export function renderRecent(stats: UsageStats, width: number, naming: ClientNam
|
|
|
602
699
|
hasOwnerInfo ? resolveClientLabel(entry.client, naming) : entry.client || "unknown"
|
|
603
700
|
);
|
|
604
701
|
const maxLabelLen = clientLabels.reduce((max, label) => Math.max(max, label.length), 6);
|
|
605
|
-
const clientCol = hasOwnerInfo ? Math.min(32, Math.max(14, maxLabelLen)) : 14;
|
|
606
702
|
|
|
607
|
-
//
|
|
703
|
+
// Token counts are drawn as `IN - OUT` with IN pinned to the left edge of the
|
|
704
|
+
// column and OUT to the right, so both halves need the widest value on show.
|
|
705
|
+
const tokenRows = recentEntries.map((entry) => tokenSegments(entry));
|
|
706
|
+
const inputTexts = tokenRows.map((row) => formatNumber(row.input));
|
|
707
|
+
const outputTexts = tokenRows.map((row) => formatNumber(row.output));
|
|
708
|
+
const inputCol = Math.max(...inputTexts.map((text) => text.length));
|
|
709
|
+
const outputCol = Math.max(...outputTexts.map((text) => text.length));
|
|
710
|
+
const inOutCol = inputCol + outputCol + " - ".length;
|
|
711
|
+
|
|
712
|
+
// Lay out the columns against the box's inner width: start from the widest
|
|
713
|
+
// layout, hand the slack to the provider column, then give space back in
|
|
714
|
+
// priority order (bar, provider, model, client) until everything fits.
|
|
608
715
|
const innerWidth = Math.max(0, width - 4);
|
|
609
|
-
|
|
716
|
+
let modelCol = 18;
|
|
717
|
+
let clientCol = hasOwnerInfo ? Math.min(32, Math.max(14, maxLabelLen)) : 14;
|
|
718
|
+
let barWidth = maxBarWidth;
|
|
719
|
+
let showProvider = false;
|
|
720
|
+
let providerCol = 0;
|
|
721
|
+
// Column widths plus one separator between each visible column.
|
|
722
|
+
const usedWidth = () =>
|
|
723
|
+
timeCol + modelCol + (barWidth + 1 + inOutCol) + costCol + clientCol +
|
|
724
|
+
(showProvider ? providerCol + 1 : 0) + 4;
|
|
725
|
+
|
|
726
|
+
if (innerWidth - usedWidth() > minProviderCol + 1) {
|
|
727
|
+
showProvider = true;
|
|
728
|
+
providerCol = innerWidth - usedWidth();
|
|
729
|
+
}
|
|
730
|
+
while (usedWidth() > innerWidth && barWidth > minBarWidth) barWidth -= 1;
|
|
731
|
+
while (usedWidth() > innerWidth && providerCol > minProviderCol) providerCol -= 1;
|
|
732
|
+
while (usedWidth() > innerWidth && modelCol > minModelCol) modelCol -= 1;
|
|
733
|
+
while (usedWidth() > innerWidth && clientCol > minClientCol) clientCol -= 1;
|
|
734
|
+
|
|
735
|
+
const tokensCol = barWidth + 1 + inOutCol;
|
|
610
736
|
|
|
611
|
-
const msatsToSats = (msats?: number) => typeof msats === "number" ? msats / 1000 : 0;
|
|
612
737
|
const lines: string[] = [];
|
|
613
|
-
|
|
614
|
-
|
|
738
|
+
const header = [
|
|
739
|
+
"TIME".padEnd(timeCol),
|
|
740
|
+
"MODEL".padEnd(modelCol),
|
|
741
|
+
"CACHE HIT".padEnd(tokensCol - inOutCol) + "IN".padEnd(inputCol) + " - " + "OUT".padStart(outputCol),
|
|
742
|
+
// Right-aligned like the values below it, so the column's right edge is
|
|
743
|
+
// shared by the header and every `n sats` cell.
|
|
744
|
+
"COST".padStart(costCol),
|
|
745
|
+
...(showProvider ? ["BASE:PROVIDER".padEnd(providerCol)] : []),
|
|
746
|
+
"CLIENT".padEnd(clientCol),
|
|
747
|
+
];
|
|
748
|
+
lines.push(`${COLORS.bold}${header.join(" ")}${COLORS.reset}`);
|
|
749
|
+
lines.push(COLORS.dim + "─".repeat(innerWidth) + COLORS.reset);
|
|
615
750
|
|
|
616
751
|
for (let i = 0; i < recentEntries.length; i++) {
|
|
617
752
|
const entry = recentEntries[i]!;
|
|
618
753
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
619
754
|
const model = entry.modelId.slice(0, modelCol).padEnd(modelCol);
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
]
|
|
755
|
+
const segments = tokenRows[i]!;
|
|
756
|
+
const bar = renderStackedBar([
|
|
757
|
+
{ value: segments.cacheRead, color: TOKEN_BAR_COLORS.cacheRead },
|
|
758
|
+
{ value: segments.notCached, color: TOKEN_BAR_COLORS.notCached },
|
|
759
|
+
], barWidth);
|
|
760
|
+
const tokens = `${bar} ${inputTexts[i]!.padEnd(inputCol)} - ${outputTexts[i]!.padStart(outputCol)}`;
|
|
626
761
|
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
formatCost(msatsToSats(entry.outputMsats)),
|
|
630
|
-
formatCost(totalSats),
|
|
631
|
-
].join("/");
|
|
762
|
+
// Right-aligned so the `sats` unit ends at the same column on every row.
|
|
763
|
+
const cost = `${formatCost(totalSats)} sats`.padStart(costCol);
|
|
632
764
|
const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "");
|
|
633
765
|
const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol);
|
|
634
766
|
const clientLabel = clientLabels[i]!.slice(0, clientCol).padEnd(clientCol);
|
|
635
767
|
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
636
768
|
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
637
|
-
lines.push(
|
|
769
|
+
lines.push([
|
|
770
|
+
`${COLORS.dim}${time}${COLORS.reset}`,
|
|
771
|
+
`${modelColor}${model}${COLORS.reset}`,
|
|
772
|
+
tokens,
|
|
773
|
+
`${COLORS.green}${cost.padEnd(costCol)}${COLORS.reset}`,
|
|
774
|
+
...(showProvider ? [`${COLORS.dim}${provider}${COLORS.reset}`] : []),
|
|
775
|
+
`${clientColor}${clientLabel}${COLORS.reset}`,
|
|
776
|
+
].join(" "));
|
|
638
777
|
}
|
|
639
778
|
|
|
640
779
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
package/src/TUI refactor.md
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# TUI refactor plan
|
|
2
|
-
|
|
3
|
-
## Goals
|
|
4
|
-
- Move the usage TUI implementation out of `src/cli/usage-tui.ts` into a dedicated `src/tui/` folder.
|
|
5
|
-
- Reduce the size and responsibility of the current monolithic file.
|
|
6
|
-
- Keep the existing CLI entrypoint stable so current usage does not break.
|
|
7
|
-
- Preserve behavior while making future TUI work easier.
|
|
8
|
-
|
|
9
|
-
## Current state
|
|
10
|
-
`src/cli/usage-tui.ts` currently mixes several concerns in one file:
|
|
11
|
-
- TUI-specific types and constants
|
|
12
|
-
- ANSI/terminal helpers
|
|
13
|
-
- scroll/search/vim navigation state
|
|
14
|
-
- data fetching from the daemon
|
|
15
|
-
- usage aggregation/stat helpers
|
|
16
|
-
- rendering for all tabs
|
|
17
|
-
- app lifecycle and keyboard event handling
|
|
18
|
-
|
|
19
|
-
This makes the file hard to extend safely.
|
|
20
|
-
|
|
21
|
-
## Refactor strategy
|
|
22
|
-
Do this incrementally and keep a thin compatibility wrapper in `src/cli/usage-tui.ts`.
|
|
23
|
-
|
|
24
|
-
### Target structure
|
|
25
|
-
- `src/tui/usage/index.ts`
|
|
26
|
-
- public entrypoint: `runUsageTui()`
|
|
27
|
-
- `src/tui/usage/types.ts`
|
|
28
|
-
- `UsageStats`, tab ids, tab metadata, derived stat types
|
|
29
|
-
- `src/tui/usage/constants.ts`
|
|
30
|
-
- tabs, colors, model/client color maps
|
|
31
|
-
- `src/tui/usage/terminal.ts`
|
|
32
|
-
- ANSI helpers, width/height helpers, `stripAnsi`
|
|
33
|
-
- `src/tui/usage/state.ts`
|
|
34
|
-
- vim/search/scroll state and state mutation helpers
|
|
35
|
-
- `src/tui/usage/data.ts`
|
|
36
|
-
- `fetchUsage()` and usage aggregation helpers
|
|
37
|
-
- `src/tui/usage/render.ts`
|
|
38
|
-
- shared render helpers and tab renderers
|
|
39
|
-
- `src/tui/usage/app.ts`
|
|
40
|
-
- main loop, render orchestration, input handling, cleanup
|
|
41
|
-
- `src/cli/usage-tui.ts`
|
|
42
|
-
- compatibility wrapper that re-exports or calls `runUsageTui()` from `src/tui/usage`
|
|
43
|
-
|
|
44
|
-
## Design choices
|
|
45
|
-
### 1. Keep CLI path compatibility
|
|
46
|
-
Do not delete the CLI file outright. Turn it into a tiny wrapper:
|
|
47
|
-
- minimal import from `../tui/usage/index.ts`
|
|
48
|
-
- export `runUsageTui()`
|
|
49
|
-
|
|
50
|
-
This avoids breaking any existing imports or scripts.
|
|
51
|
-
|
|
52
|
-
### 2. Separate pure logic from side effects
|
|
53
|
-
Keep these pure where possible:
|
|
54
|
-
- aggregation helpers
|
|
55
|
-
- formatting helpers
|
|
56
|
-
- render helpers that return strings
|
|
57
|
-
- scroll clamping logic
|
|
58
|
-
|
|
59
|
-
Keep side effects isolated in the app layer:
|
|
60
|
-
- reading terminal size
|
|
61
|
-
- writing to stdout
|
|
62
|
-
- raw mode setup
|
|
63
|
-
- signal handling
|
|
64
|
-
- interval scheduling
|
|
65
|
-
|
|
66
|
-
### 3. Avoid over-engineering
|
|
67
|
-
This should be a pragmatic refactor, not a framework:
|
|
68
|
-
- no unnecessary classes
|
|
69
|
-
- keep function-based design
|
|
70
|
-
- only extract modules around clear responsibility boundaries
|
|
71
|
-
|
|
72
|
-
### 4. Preserve behavior first
|
|
73
|
-
No UX changes unless needed to support the extraction.
|
|
74
|
-
That means:
|
|
75
|
-
- same tabs
|
|
76
|
-
- same keybindings
|
|
77
|
-
- same output format
|
|
78
|
-
- same fetch cadence
|
|
79
|
-
- same search/scroll behavior
|
|
80
|
-
|
|
81
|
-
## Implementation steps
|
|
82
|
-
1. Create `src/tui/usage/`.
|
|
83
|
-
2. Extract types/constants first.
|
|
84
|
-
3. Extract terminal helpers.
|
|
85
|
-
4. Extract data fetching + aggregation helpers.
|
|
86
|
-
5. Extract state/search/scroll logic.
|
|
87
|
-
6. Extract rendering helpers + tab renderers.
|
|
88
|
-
7. Build `app.ts` using the extracted modules.
|
|
89
|
-
8. Replace `src/cli/usage-tui.ts` with a thin wrapper.
|
|
90
|
-
9. Run a TypeScript/bun check and fix imports.
|
|
91
|
-
10. Smoke-test keyboard handling and rendering behavior.
|
|
92
|
-
|
|
93
|
-
## Risks
|
|
94
|
-
- circular imports between render/state/constants
|
|
95
|
-
- broken relative import paths during extraction
|
|
96
|
-
- subtle behavior regressions in scroll/search state
|
|
97
|
-
- terminal escape handling differences if helpers are split carelessly
|
|
98
|
-
|
|
99
|
-
## Validation checklist
|
|
100
|
-
- `src/cli/usage-tui.ts` still exposes `runUsageTui()`
|
|
101
|
-
- TUI starts from the same CLI path
|
|
102
|
-
- scroll still works for long content
|
|
103
|
-
- vim keys still work
|
|
104
|
-
- arrow keys still work
|
|
105
|
-
- tab switching still resets scroll
|
|
106
|
-
- search mode still works
|
|
107
|
-
- cleanup still restores cursor and alternate screen
|
|
108
|
-
|
|
109
|
-
## Non-goals
|
|
110
|
-
- redesigning the UI
|
|
111
|
-
- changing tab contents
|
|
112
|
-
- introducing tests unless needed for safety
|
|
113
|
-
- adding new features unrelated to the refactor
|