pi-cliproxy-usage 0.1.1 → 0.2.1

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 CHANGED
@@ -2,13 +2,6 @@
2
2
 
3
3
  Compact CLIProxyAPI account usage meters for Pi Coding Agent.
4
4
 
5
- Code layout:
6
-
7
- - `index.ts` — Pi lifecycle and command wiring
8
- - `src/settings.ts` / `src/settings-ui.ts` — persisted and interactive settings
9
- - `src/usage.ts` / `src/parsers.ts` — account discovery, provider requests, response mapping
10
- - `src/ui.ts` — widget rendering and text formatting
11
-
12
5
  Shows one colored line below editor per enabled account:
13
6
 
14
7
  ```text
@@ -19,12 +12,19 @@ Shows one colored line below editor per enabled account:
19
12
 
20
13
  Percentages and filled bars show usage **consumed**. Colors shift green → yellow at 70% → red at 90%. Codex intentionally shows Session only; weekly meter is omitted.
21
14
 
15
+ ## Requirements
16
+
17
+ - [Pi Coding Agent](https://github.com/earendil-works/pi)
18
+ - [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) with at least one Claude, Codex, or Grok account configured
19
+
22
20
  ## Install
23
21
 
24
22
  ```bash
25
23
  pi install npm:pi-cliproxy-usage
26
24
  ```
27
25
 
26
+ After installing or updating, run `/reload` in Pi.
27
+
28
28
  Or test directly:
29
29
 
30
30
  ```bash
@@ -41,6 +41,7 @@ Older `~/.pi/agent/extensions/pi-cliproxy-usage/config.json` files migrate autom
41
41
  {
42
42
  "accountsDir": "~/.cli-proxy-api",
43
43
  "refreshMinutes": 5,
44
+ "maxVisibleAccounts": 4,
44
45
  "providers": {
45
46
  "claude": true,
46
47
  "codex": true,
@@ -51,7 +52,7 @@ Older `~/.pi/agent/extensions/pi-cliproxy-usage/config.json` files migrate autom
51
52
 
52
53
  Extension reads top-level CLIProxyAPI auth JSON files with `type` equal to `claude`, `codex`, or `xai`. Disabled auth files are skipped. Credentials stay local and are sent only to official provider usage endpoints documented by OpenUsage.
53
54
 
54
- Accepted values: `accountsDir` is a non-empty string, `refreshMinutes` is an integer of at least `1`, and provider values are booleans. Invalid values are ignored with a warning. Unknown fields are preserved when saving.
55
+ Accepted values: `accountsDir` is a non-empty string; `refreshMinutes` and `maxVisibleAccounts` are integers of at least `1`; provider values are booleans. Widget prioritizes errors, then accounts with highest usage, and shows an overflow row when more accounts exist. Invalid values are ignored with a warning. Unknown fields are preserved when saving.
55
56
 
56
57
  ## Commands
57
58
 
@@ -62,3 +63,11 @@ Accepted values: `accountsDir` is a non-empty string, `refreshMinutes` is an int
62
63
  - `/cliproxy-usage config` — compatibility alias for `settings`
63
64
 
64
65
  Token refresh is deliberately left to CLIProxyAPI. If provider returns `401`/`403`, let CLIProxyAPI refresh account or log in again.
66
+
67
+ ## Screenshot
68
+
69
+ ![CLIProxyAPI usage widget](https://raw.githubusercontent.com/Villoh/pi-cliproxy-usage/main/assets/preview.png)
70
+
71
+ ## Support
72
+
73
+ Report bugs and request features in [GitHub Issues](https://github.com/Villoh/pi-cliproxy-usage/issues).
package/index.ts CHANGED
@@ -31,7 +31,7 @@ export default function (pi: ExtensionAPI) {
31
31
  ctx.ui.notify(loaded.warnings.join("; "), "warning");
32
32
  }
33
33
  const items = await readAccounts(loaded.settings);
34
- renderUsage(ctx, items);
34
+ renderUsage(ctx, items, loaded.settings.maxVisibleAccounts);
35
35
  if (notify) {
36
36
  ctx.ui.notify(
37
37
  formatDetails(items),
@@ -48,8 +48,19 @@ export default function (pi: ExtensionAPI) {
48
48
  timer.unref?.();
49
49
  };
50
50
 
51
- const applySettings = async (ctx: ExtensionContext, settings: Settings) => {
51
+ const applySettings = async (
52
+ ctx: ExtensionContext,
53
+ settings: Settings,
54
+ changedId: string,
55
+ ) => {
52
56
  scheduleRefresh(ctx, settings.refreshMinutes);
57
+ // Only refetch when something that affects the fetched data changed.
58
+ // Interval/display-only changes (refreshMinutes, maxVisibleAccounts) must
59
+ // not trigger a request, or every settings tweak burns an API call and
60
+ // risks rate limiting.
61
+ if (changedId === "refreshMinutes" || changedId === "maxVisibleAccounts") {
62
+ return;
63
+ }
53
64
  await refresh(ctx);
54
65
  };
55
66
 
@@ -64,6 +75,7 @@ export default function (pi: ExtensionAPI) {
64
75
  `Settings: ${loaded.path}`,
65
76
  `Accounts: ${loaded.settings.accountsDir}`,
66
77
  `Refresh: ${loaded.settings.refreshMinutes} min`,
78
+ `Visible accounts: ${loaded.settings.maxVisibleAccounts}`,
67
79
  `Providers: ${providers || "none"}`,
68
80
  `Source: ${Object.keys(loaded.raw).length ? "settings file" : "defaults"}`,
69
81
  ].join("\n"),
@@ -103,7 +115,7 @@ export default function (pi: ExtensionAPI) {
103
115
  ctx,
104
116
  SETTINGS_PATH,
105
117
  LEGACY_SETTINGS_PATH,
106
- (settings) => applySettings(ctx, settings),
118
+ (settings, changedId) => applySettings(ctx, settings, changedId),
107
119
  );
108
120
  }
109
121
  if (action === "status") return showStatus(ctx);
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "pi-cliproxy-usage",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Compact CLIProxyAPI account usage meters for Pi Coding Agent",
5
5
  "type": "module",
6
- "keywords": ["pi-package", "cliproxyapi", "usage"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "cliproxyapi",
9
+ "usage"
10
+ ],
7
11
  "license": "MIT",
8
12
  "repository": {
9
13
  "type": "git",
10
14
  "url": "git+https://github.com/Villoh/pi-cliproxy-usage.git"
11
15
  },
12
- "files": ["index.ts", "src", "README.md"],
16
+ "files": [
17
+ "index.ts",
18
+ "src",
19
+ "README.md"
20
+ ],
13
21
  "pi": {
14
- "extensions": ["./index.ts"]
22
+ "extensions": [
23
+ "./index.ts"
24
+ ]
15
25
  },
16
26
  "peerDependencies": {
17
27
  "@earendil-works/pi-coding-agent": "*",
@@ -18,7 +18,7 @@ export async function showSettings(
18
18
  ctx: ExtensionCommandContext,
19
19
  settingsPath: string,
20
20
  legacySettingsPath: string,
21
- onChange: (settings: Settings) => Promise<void>,
21
+ onChange: (settings: Settings, changedId: string) => Promise<void>,
22
22
  ): Promise<void> {
23
23
  if (ctx.mode !== "tui") {
24
24
  if (ctx.hasUI)
@@ -55,11 +55,18 @@ export async function showSettings(
55
55
  },
56
56
  {
57
57
  id: "refreshMinutes",
58
- label: "Refresh interval",
58
+ label: "Refresh interval (min)",
59
59
  description: "Minutes between automatic usage refreshes",
60
60
  currentValue: String(settings.refreshMinutes),
61
61
  values: ["1", "5", "10", "15", "30", "60"],
62
62
  },
63
+ {
64
+ id: "maxVisibleAccounts",
65
+ label: "Visible accounts",
66
+ description: "Maximum account rows shown below editor",
67
+ currentValue: String(settings.maxVisibleAccounts),
68
+ values: ["1", "2", "3", "4", "5", "10"],
69
+ },
63
70
  ...(["claude", "codex", "grok"] as const).map((provider) => ({
64
71
  id: provider,
65
72
  label: `${provider[0]?.toUpperCase()}${provider.slice(1)}`,
@@ -84,6 +91,9 @@ export async function showSettings(
84
91
  const previous = structuredClone(settings);
85
92
  if (id === "accountsDir") settings.accountsDir = value;
86
93
  if (id === "refreshMinutes") settings.refreshMinutes = Number(value);
94
+ if (id === "maxVisibleAccounts") {
95
+ settings.maxVisibleAccounts = Number(value);
96
+ }
87
97
  if (providerIds.has(id as ProviderName)) {
88
98
  settings.providers[id as ProviderName] = value === "enabled";
89
99
  }
@@ -91,7 +101,7 @@ export async function showSettings(
91
101
  saveQueue = saveQueue
92
102
  .then(async () => {
93
103
  raw = await saveSettings(next, raw, settingsPath);
94
- await onChange(next);
104
+ await onChange(next, id);
95
105
  })
96
106
  .catch((error) => {
97
107
  settings = previous;
@@ -99,6 +109,8 @@ export async function showSettings(
99
109
  if (id === "accountsDir") previousValue = previous.accountsDir;
100
110
  else if (id === "refreshMinutes") {
101
111
  previousValue = String(previous.refreshMinutes);
112
+ } else if (id === "maxVisibleAccounts") {
113
+ previousValue = String(previous.maxVisibleAccounts);
102
114
  } else {
103
115
  previousValue = previous.providers[id as ProviderName]
104
116
  ? "enabled"
package/src/settings.ts CHANGED
@@ -5,6 +5,7 @@ import type { Settings } from "./types.js";
5
5
  export const DEFAULT_SETTINGS: Settings = {
6
6
  accountsDir: "~/.cli-proxy-api",
7
7
  refreshMinutes: 5,
8
+ maxVisibleAccounts: 4,
8
9
  providers: { claude: true, codex: true, grok: true },
9
10
  };
10
11
 
@@ -47,6 +48,15 @@ export function normalizeSettings(value: unknown): {
47
48
  } else if (value.refreshMinutes !== undefined) {
48
49
  warnings.push("ignored invalid refreshMinutes");
49
50
  }
51
+ if (
52
+ typeof value.maxVisibleAccounts === "number" &&
53
+ Number.isInteger(value.maxVisibleAccounts) &&
54
+ value.maxVisibleAccounts > 0
55
+ ) {
56
+ settings.maxVisibleAccounts = value.maxVisibleAccounts;
57
+ } else if (value.maxVisibleAccounts !== undefined) {
58
+ warnings.push("ignored invalid maxVisibleAccounts");
59
+ }
50
60
  for (const provider of ["claude", "codex", "grok"] as const) {
51
61
  if (typeof providers[provider] === "boolean") {
52
62
  settings.providers[provider] = providers[provider];
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ export type ProviderName = "claude" | "codex" | "grok";
3
3
  export type Settings = {
4
4
  accountsDir: string;
5
5
  refreshMinutes: number;
6
+ maxVisibleAccounts: number;
6
7
  providers: Record<ProviderName, boolean>;
7
8
  };
8
9
 
package/src/ui.ts CHANGED
@@ -13,8 +13,7 @@ const PROVIDER_LABELS: Record<ProviderName, string> = {
13
13
  };
14
14
 
15
15
  function accountLabel(label: string): string {
16
- const local = label.includes("@") ? label.split("@")[0] : label;
17
- return local.slice(0, 18);
16
+ return label;
18
17
  }
19
18
 
20
19
  export function usageBar(used: number, width = 10): string {
@@ -80,18 +79,37 @@ export function clearUsage(ctx: UiContext): void {
80
79
  ctx.ui.setWidget("cliproxy-usage", undefined);
81
80
  }
82
81
 
83
- export function renderUsage(ctx: UiContext, items: AccountUsage[]): void {
82
+ export function renderUsage(
83
+ ctx: UiContext,
84
+ items: AccountUsage[],
85
+ maxVisibleAccounts: number,
86
+ ): void {
84
87
  ctx.ui.setStatus("cliproxy-usage", undefined);
85
88
  if (!items.length) {
86
89
  ctx.ui.setWidget("cliproxy-usage", undefined);
87
90
  return;
88
91
  }
92
+ const visibleItems = items
93
+ .map((item, index) => ({
94
+ item,
95
+ index,
96
+ priority: item.error
97
+ ? Number.POSITIVE_INFINITY
98
+ : Math.max(item.session?.used ?? -1, item.weekly?.used ?? -1),
99
+ }))
100
+ .sort(
101
+ (left, right) =>
102
+ right.priority - left.priority || left.index - right.index,
103
+ )
104
+ .slice(0, maxVisibleAccounts)
105
+ .map(({ item }) => item);
106
+ const hiddenCount = items.length - visibleItems.length;
89
107
  ctx.ui.setWidget(
90
108
  "cliproxy-usage",
91
109
  (_tui: unknown, theme: Theme) => ({
92
110
  invalidate() {},
93
111
  render(width: number): string[] {
94
- return items.map((item) => {
112
+ const lines = visibleItems.map((item) => {
95
113
  const providerColor =
96
114
  item.provider === "claude"
97
115
  ? "warning"
@@ -127,6 +145,18 @@ export function renderUsage(ctx: UiContext, items: AccountUsage[]): void {
127
145
  width,
128
146
  );
129
147
  });
148
+ if (hiddenCount) {
149
+ lines.push(
150
+ truncateAnsi(
151
+ theme.fg(
152
+ "dim",
153
+ `… ${hiddenCount} more account${hiddenCount === 1 ? "" : "s"} · /cliproxy-usage for details`,
154
+ ),
155
+ width,
156
+ ),
157
+ );
158
+ }
159
+ return lines;
130
160
  },
131
161
  }),
132
162
  { placement: "belowEditor" },