pi-cliproxyapi 0.2.0 → 0.3.0
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 +34 -23
- package/index.ts +3 -3
- package/package.json +1 -1
- package/src/commands.ts +12 -143
- package/src/fetch-models.ts +2 -5
- package/src/log.ts +17 -4
- package/src/ui-hub/hub.ts +264 -0
- package/src/ui-hub/index.ts +50 -0
- package/src/ui-hub/shell.ts +119 -0
- package/src/ui-hub/types.ts +16 -0
- package/src/ui-hub/view-diagnostics.ts +108 -0
- package/src/ui-hub/view-models.ts +515 -0
- package/src/ui-hub/view-usage.ts +131 -0
- package/src/ui-picker/mutate.ts +34 -0
- package/src/ui-setup.ts +1 -1
- package/src/ui-usage.ts +1 -1
- package/src/ui-overlay.ts +0 -235
- package/src/ui-picker/index.ts +0 -35
- package/src/ui-picker/picker-component.ts +0 -432
- package/src/ui-picker/picker.ts +0 -247
package/README.md
CHANGED
|
@@ -9,37 +9,44 @@ One `(endpoint, apiKey)` pair — every provider and model inherits it automatic
|
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
+
- **Unified hub** — one `/cliproxy` overlay with **Models / Usage / Diagnostics** tabs (number hotkeys `1` `2` `3`) plus global actions: `r` refresh, `e` setup, `s` save
|
|
12
13
|
- **Built-in provider routing** — whitelist which Anthropic / OpenAI / etc. models are available through the proxy
|
|
13
14
|
- **Custom provider groups** — create named groups (e.g. `corp-glm`, `corp-gemini`) for proxy-only models with automatic metadata from [models.dev](https://models.dev)
|
|
14
|
-
- **Exclusive model pool** — a model assigned to one group automatically disappears from others
|
|
15
|
-
- **
|
|
15
|
+
- **Exclusive model pool** — a model assigned to one group automatically disappears from others, grouped by `owned_by` with type-to-filter (`/`)
|
|
16
|
+
- **Live save state** — the header shows `● unsaved` while you edit and `✓ settings saved` after `s`, no console noise
|
|
17
|
+
- **Per-account usage tab** — colored quota bars, toggle disabled accounts, verbose errors — no LLM call
|
|
16
18
|
- **Setup wizard** — `/cliproxy-setup` configures endpoint, API key, provider prefix, and usage key interactively
|
|
17
19
|
|
|
18
20
|
## Commands
|
|
19
21
|
|
|
22
|
+
Two commands; everything else lives inside the hub as tabs and actions.
|
|
23
|
+
|
|
20
24
|
| Command | Description |
|
|
21
25
|
| --- | --- |
|
|
22
|
-
| `/cliproxy` |
|
|
26
|
+
| `/cliproxy` | Hub overlay — **Models** / **Usage** / **Diagnostics** tabs plus global actions |
|
|
23
27
|
| `/cliproxy-setup` | Configure endpoint, API key, provider prefix, usage key |
|
|
24
|
-
| `/cliproxy-refresh` | Re-fetch upstream models, re-register providers |
|
|
25
|
-
| `/cliproxy-usage` | Per-account quota windows with progress bars (`d` = show disabled, `v` = verbose) |
|
|
26
|
-
| `/cliproxy-doctor` | Connectivity, key resolution, discovery diagnostics |
|
|
27
28
|
|
|
28
|
-
### `/cliproxy`
|
|
29
|
+
### The `/cliproxy` hub
|
|
30
|
+
|
|
31
|
+
Global keys: `[` / `]` or `1` `2` `3` switch tabs · `r` refresh discovery + reapply · `e` setup · `s` save · `q` / `Esc` close.
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
**Models tab** — three panels cycled with `Tab` / arrows:
|
|
31
34
|
|
|
32
35
|
- **left** — every provider (built-in + custom). `+ new custom group…` is the last row.
|
|
33
|
-
- **right top** — models
|
|
34
|
-
- **right bottom** — available
|
|
36
|
+
- **right top** — models assigned to the focused provider. `Enter` / `Space` removes one.
|
|
37
|
+
- **right bottom** — available pool, grouped by upstream `owned_by`. `Enter` / `Space` attaches. Press `/` to filter the pool by id/name. A `⚠` marks an API mismatch (attach still allowed).
|
|
38
|
+
|
|
39
|
+
Extra Models keys: `d` removes a custom group (with confirmation).
|
|
40
|
+
|
|
41
|
+
**Usage tab** — per-account quota bars; `d` shows disabled accounts, `v` shows verbose errors.
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
**Diagnostics tab** — connectivity, key resolution, and discovery shape.
|
|
37
44
|
|
|
38
45
|
## Prerequisites
|
|
39
46
|
|
|
40
47
|
You need a running [CliProxyAPI](https://github.com/router-for-me/CLIProxyAPI) instance — this is the corporate LLM proxy that aggregates multiple providers behind a single OpenAI-compatible endpoint.
|
|
41
48
|
|
|
42
|
-
For full functionality (
|
|
49
|
+
For full functionality (Usage tab, enriched model metadata from [models.dev](https://models.dev)), also deploy the companion sidecar: **[pi-cliproxyapi-wellknown](https://github.com/abix5/pi-cliproxyapi-wellknown)**. See [Deploying the sidecar](#deploying-the-sidecar-service) below.
|
|
43
50
|
|
|
44
51
|
## Install
|
|
45
52
|
|
|
@@ -85,7 +92,7 @@ The plugin tries `GET <endpoint-origin>/.well-known/pi` first (requires the side
|
|
|
85
92
|
The **[pi-cliproxyapi-wellknown](https://github.com/abix5/pi-cliproxyapi-wellknown)** sidecar runs alongside CliProxyAPI and provides:
|
|
86
93
|
|
|
87
94
|
- `/.well-known/pi` — model discovery with metadata from [models.dev](https://models.dev) (context windows, costs, reasoning flags)
|
|
88
|
-
- `/api/usage` — per-account quota windows used by
|
|
95
|
+
- `/api/usage` — per-account quota windows used by the hub Usage tab
|
|
89
96
|
|
|
90
97
|
```
|
|
91
98
|
┌──────────────┐ ┌───────────────────────────┐
|
|
@@ -143,16 +150,16 @@ Run `/cliproxy-setup` in Pi and enter:
|
|
|
143
150
|
- **endpoint** — your public proxy URL ending with `/v1`
|
|
144
151
|
- **apiKey** — CliProxyAPI bearer key
|
|
145
152
|
- **providerPrefix** — short slug for custom provider names (e.g. `corp`, `myproxy`)
|
|
146
|
-
- **usageKey** — same value as `PI_PLUGIN_USAGE_KEY` above (enables
|
|
153
|
+
- **usageKey** — same value as `PI_PLUGIN_USAGE_KEY` above (enables the Usage tab)
|
|
147
154
|
|
|
148
155
|
The sidecar is **optional for basic usage** — without it the plugin falls back to raw `/v1/models` with local heuristics. What changes:
|
|
149
156
|
|
|
150
157
|
| | With sidecar | Without sidecar |
|
|
151
158
|
| --- | --- | --- |
|
|
152
159
|
| Model discovery | Enriched from [models.dev](https://models.dev) (real context windows, costs, reasoning) | Defaults: `contextWindow=128k`, `maxTokens=16k`, `cost=0`, `reasoning=false` |
|
|
153
|
-
|
|
|
160
|
+
| Usage tab | Works — per-account quota bars | **Does not work** (no `/api/usage` endpoint) |
|
|
154
161
|
| Classification | Server-side, accurate | Local heuristics by `owned_by` |
|
|
155
|
-
| `/cliproxy
|
|
162
|
+
| `/cliproxy` hub | Works | Works (Usage tab shows an error) |
|
|
156
163
|
|
|
157
164
|
## Layout
|
|
158
165
|
|
|
@@ -160,26 +167,30 @@ The sidecar is **optional for basic usage** — without it the plugin falls back
|
|
|
160
167
|
index.ts ExtensionFactory entry point
|
|
161
168
|
src/
|
|
162
169
|
config.ts ~/.config/pi-cliproxyapi/config.json
|
|
163
|
-
commands.ts
|
|
170
|
+
commands.ts 2 slash commands (hub + setup)
|
|
164
171
|
apply.ts pi.registerProvider calls
|
|
165
172
|
fetch-models.ts well-known + /v1/models fallback
|
|
166
173
|
fetch-usage.ts /api/usage client with TTL cache
|
|
167
174
|
compat.ts baseUrl derivation, model classification
|
|
168
175
|
conflicts.ts read-only ~/.pi/{models,auth}.json scan
|
|
169
176
|
ui-frame.ts single source of truth for overlay frames
|
|
170
|
-
ui-overlay.ts scrollable overlay shell with toggles
|
|
171
177
|
ui-setup.ts setup wizard
|
|
172
178
|
ui-usage.ts ANSI-coloured usage renderer
|
|
173
|
-
ui-
|
|
174
|
-
index.ts public
|
|
179
|
+
ui-hub/ the /cliproxy hub overlay
|
|
180
|
+
index.ts public runHub entry
|
|
181
|
+
hub.ts tabs, status header, global actions
|
|
182
|
+
types.ts HubView contract
|
|
183
|
+
shell.ts tab bar, status header, scroll/slice helpers
|
|
184
|
+
view-models.ts three-panel picker (single pool ordering + filter)
|
|
185
|
+
view-usage.ts usage tab (lazy fetch + d/v toggles)
|
|
186
|
+
view-diagnostics.ts diagnostics tab
|
|
187
|
+
ui-picker/ picker building blocks reused by the Models view
|
|
175
188
|
types.ts shared TS types
|
|
176
189
|
catalog.ts build a model lookup from discovery
|
|
177
190
|
providers.ts resolve the providers shown in the left panel
|
|
178
|
-
mutate.ts attach / detach / claim
|
|
191
|
+
mutate.ts attach / detach / claim + pool grouping + display order
|
|
179
192
|
render-text.ts ANSI-aware pad / truncate
|
|
180
193
|
rows.ts per-row renderers for left / right panels
|
|
181
|
-
picker.ts state + navigation, glues catalogue to UI
|
|
182
|
-
picker-component.ts render + input dispatch
|
|
183
194
|
prompt-confirm.ts remove-group confirmation
|
|
184
195
|
prompt-name.ts new-group name prompt
|
|
185
196
|
log.ts tagged logger
|
package/index.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* 1. load ~/.config/pi-cliproxyapi/config.json (defaults if missing)
|
|
7
7
|
* 2. fetch discovery (well-known → fall back to /v1/models)
|
|
8
8
|
* 3. call pi.registerProvider for each enabled built-in + custom provider
|
|
9
|
-
* 4. register slash commands /cliproxy /cliproxy-setup
|
|
10
|
-
*
|
|
9
|
+
* 4. register slash commands /cliproxy and /cliproxy-setup
|
|
10
|
+
* (refresh, usage, and diagnostics are tabs/actions inside the hub)
|
|
11
11
|
*
|
|
12
12
|
* All discovery + apply errors are logged but never abort extension load —
|
|
13
13
|
* a missing/broken proxy must not prevent Pi from starting.
|
|
@@ -43,7 +43,7 @@ export default async function cliproxyapi(pi: ExtensionAPI): Promise<void> {
|
|
|
43
43
|
await applyAll(pi, cfg, discovery);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
log.error("initial apply failed:", (err as Error).message);
|
|
46
|
-
// Commands stay registered; user can /cliproxy
|
|
46
|
+
// Commands stay registered; user can open /cliproxy and press r to refresh.
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (cfg.refreshIntervalMinutes > 0) {
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// Slash commands
|
|
2
|
-
// /cliproxy
|
|
3
|
-
// /cliproxy-setup
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// /cliproxy-doctor — connectivity + key-resolution diagnostics
|
|
1
|
+
// Slash commands (2):
|
|
2
|
+
// /cliproxy — open the hub (Models / Usage / Diagnostics + actions)
|
|
3
|
+
// /cliproxy-setup — first-run / re-run wizard for endpoint+keys
|
|
4
|
+
//
|
|
5
|
+
// Refresh, usage, and diagnostics are now actions/tabs inside the hub.
|
|
7
6
|
|
|
8
7
|
import type {
|
|
9
8
|
ExtensionAPI,
|
|
@@ -11,20 +10,16 @@ import type {
|
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
12
11
|
|
|
13
12
|
import { applyAll } from "./apply.ts";
|
|
14
|
-
import { loadConfig, resolveConfigValue
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { log } from "./log.ts";
|
|
19
|
-
import { showOverlay } from "./ui-overlay.ts";
|
|
20
|
-
import { runPicker } from "./ui-picker/index.ts";
|
|
13
|
+
import { loadConfig, resolveConfigValue } from "./config.ts";
|
|
14
|
+
import { fetchDiscovery } from "./fetch-models.ts";
|
|
15
|
+
import { clearUsageCache } from "./fetch-usage.ts";
|
|
16
|
+
import { runHub } from "./ui-hub/index.ts";
|
|
21
17
|
import { runSetup } from "./ui-setup.ts";
|
|
22
|
-
import { renderUsage } from "./ui-usage.ts";
|
|
23
18
|
|
|
24
19
|
export function registerCommands(pi: ExtensionAPI): void {
|
|
25
20
|
pi.registerCommand("cliproxy", {
|
|
26
21
|
description:
|
|
27
|
-
"
|
|
22
|
+
"Manage proxy models, usage, and diagnostics in one hub overlay",
|
|
28
23
|
handler: handleCliproxy.bind(null, pi),
|
|
29
24
|
});
|
|
30
25
|
|
|
@@ -33,22 +28,6 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
33
28
|
"Set endpoint, API key, and (optional) usage key for the proxy",
|
|
34
29
|
handler: handleSetup.bind(null, pi),
|
|
35
30
|
});
|
|
36
|
-
|
|
37
|
-
pi.registerCommand("cliproxy-refresh", {
|
|
38
|
-
description:
|
|
39
|
-
"Re-fetch upstream model list and re-apply provider registrations",
|
|
40
|
-
handler: handleRefresh.bind(null, pi),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
pi.registerCommand("cliproxy-usage", {
|
|
44
|
-
description: "Show per-account quota windows from the upstream",
|
|
45
|
-
handler: handleUsage,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
pi.registerCommand("cliproxy-doctor", {
|
|
49
|
-
description: "Check connectivity, key resolution, and discovery shape",
|
|
50
|
-
handler: handleDoctor,
|
|
51
|
-
});
|
|
52
31
|
}
|
|
53
32
|
|
|
54
33
|
// --------------------------------------------------------------------------- /cliproxy
|
|
@@ -61,7 +40,7 @@ async function handleCliproxy(
|
|
|
61
40
|
const cfg = loadConfig();
|
|
62
41
|
if (!cfg.proxy.endpoint || !resolveConfigValue(cfg.proxy.apiKey)) {
|
|
63
42
|
ctx.ui.notify(
|
|
64
|
-
"endpoint or API key not set \u2014 launching
|
|
43
|
+
"endpoint or API key not set \u2014 launching setup first",
|
|
65
44
|
"info",
|
|
66
45
|
);
|
|
67
46
|
const ok = await runSetup(ctx, true);
|
|
@@ -76,17 +55,7 @@ async function handleCliproxy(
|
|
|
76
55
|
ctx.ui.notify(`discovery failed: ${(err as Error).message}`, "error");
|
|
77
56
|
return;
|
|
78
57
|
}
|
|
79
|
-
|
|
80
|
-
if (!updated) {
|
|
81
|
-
ctx.ui.notify("changes discarded", "info");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
saveConfig(updated);
|
|
85
|
-
const rep = await applyAll(pi, updated, discovery);
|
|
86
|
-
ctx.ui.notify(
|
|
87
|
-
`saved \u00b7 ${rep.registered.length} providers registered, ${rep.skipped.length} skipped`,
|
|
88
|
-
"info",
|
|
89
|
-
);
|
|
58
|
+
await runHub(pi, ctx, current, discovery);
|
|
90
59
|
}
|
|
91
60
|
|
|
92
61
|
// --------------------------------------------------------------------------- /cliproxy-setup
|
|
@@ -118,103 +87,3 @@ async function handleSetup(
|
|
|
118
87
|
);
|
|
119
88
|
}
|
|
120
89
|
}
|
|
121
|
-
|
|
122
|
-
// --------------------------------------------------------------------------- /cliproxy-refresh
|
|
123
|
-
|
|
124
|
-
async function handleRefresh(
|
|
125
|
-
pi: ExtensionAPI,
|
|
126
|
-
_args: string,
|
|
127
|
-
ctx: ExtensionCommandContext,
|
|
128
|
-
): Promise<void> {
|
|
129
|
-
const cfg = loadConfig();
|
|
130
|
-
const resolvedKey = resolveConfigValue(cfg.proxy.apiKey);
|
|
131
|
-
try {
|
|
132
|
-
const discovery = await fetchDiscovery(cfg, resolvedKey);
|
|
133
|
-
const rep = await applyAll(pi, cfg, discovery);
|
|
134
|
-
clearUsageCache();
|
|
135
|
-
ctx.ui.notify(
|
|
136
|
-
`cliproxy: ${rep.registered.length} providers registered, ${rep.skipped.length} skipped (source=${discovery.source})`,
|
|
137
|
-
"info",
|
|
138
|
-
);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
ctx.ui.notify(`refresh failed: ${(err as Error).message}`, "error");
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// --------------------------------------------------------------------------- /cliproxy-usage
|
|
145
|
-
|
|
146
|
-
async function handleUsage(
|
|
147
|
-
args: string,
|
|
148
|
-
ctx: ExtensionCommandContext,
|
|
149
|
-
): Promise<void> {
|
|
150
|
-
const force = /(^|\s)--refresh(\s|$)/.test(args);
|
|
151
|
-
const cfg = loadConfig();
|
|
152
|
-
const usageKey = resolveConfigValue(cfg.proxy.usageKey);
|
|
153
|
-
let doc;
|
|
154
|
-
try {
|
|
155
|
-
doc = await fetchUsage(cfg, usageKey, { force });
|
|
156
|
-
} catch (err) {
|
|
157
|
-
ctx.ui.notify(`usage failed: ${(err as Error).message}`, "error");
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
await showOverlay(ctx, "cliproxy-usage", {
|
|
161
|
-
render: (state) =>
|
|
162
|
-
renderUsage(doc, {
|
|
163
|
-
showDisabled: state["d"] === true,
|
|
164
|
-
verbose: state["v"] === true,
|
|
165
|
-
}).join("\n"),
|
|
166
|
-
toggles: [
|
|
167
|
-
{ key: "d", hint: "d disabled" },
|
|
168
|
-
{ key: "v", hint: "v verbose" },
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// --------------------------------------------------------------------------- /cliproxy-doctor
|
|
174
|
-
|
|
175
|
-
async function handleDoctor(
|
|
176
|
-
_args: string,
|
|
177
|
-
ctx: ExtensionCommandContext,
|
|
178
|
-
): Promise<void> {
|
|
179
|
-
const cfg = loadConfig();
|
|
180
|
-
const lines: string[] = [];
|
|
181
|
-
lines.push(`endpoint: ${cfg.proxy.endpoint}`);
|
|
182
|
-
lines.push(
|
|
183
|
-
`apiKey resolves: ${resolveConfigValue(cfg.proxy.apiKey) ? "yes" : "NO (empty after resolution)"}`,
|
|
184
|
-
);
|
|
185
|
-
lines.push(
|
|
186
|
-
`usageKey resolves: ${cfg.proxy.usageKey ? (resolveConfigValue(cfg.proxy.usageKey) ? "yes" : "NO") : "not configured"}`,
|
|
187
|
-
);
|
|
188
|
-
lines.push(`user-agent: ${PLUGIN_USER_AGENT}`);
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const discovery = await fetchDiscovery(
|
|
192
|
-
cfg,
|
|
193
|
-
resolveConfigValue(cfg.proxy.apiKey),
|
|
194
|
-
);
|
|
195
|
-
lines.push("");
|
|
196
|
-
lines.push(`discovery source: ${discovery.source}`);
|
|
197
|
-
lines.push(`upstream version: ${discovery.upstreamVersion ?? "(unknown)"}`);
|
|
198
|
-
lines.push(`upstream total ids: ${discovery.upstreamTotal}`);
|
|
199
|
-
lines.push(
|
|
200
|
-
`built-in providers seen: ${discovery.builtinProviders.map((p) => `${p.name}=${p.models.length}`).join(", ") || "(none)"}`,
|
|
201
|
-
);
|
|
202
|
-
lines.push(`custom pool size: ${discovery.customPool.length}`);
|
|
203
|
-
} catch (err) {
|
|
204
|
-
lines.push("");
|
|
205
|
-
lines.push(`discovery FAILED: ${(err as Error).message}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const conflicts = detectConflicts(cfg);
|
|
209
|
-
if (conflicts.length > 0) {
|
|
210
|
-
lines.push("");
|
|
211
|
-
lines.push("conflicts:");
|
|
212
|
-
for (const c of conflicts) lines.push(` [${c.kind}] ${c.detail}`);
|
|
213
|
-
} else {
|
|
214
|
-
lines.push("");
|
|
215
|
-
lines.push("conflicts: none");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
log.info("doctor:", lines.join(" | "));
|
|
219
|
-
await showOverlay(ctx, "cliproxy-doctor", lines.join("\n"));
|
|
220
|
-
}
|
package/src/fetch-models.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import type { ProxyConfig } from "./config.ts";
|
|
21
21
|
import { log } from "./log.ts";
|
|
22
22
|
|
|
23
|
-
export const PLUGIN_USER_AGENT = "pi-cliproxyapi/0.
|
|
23
|
+
export const PLUGIN_USER_AGENT = "pi-cliproxyapi/0.3.0";
|
|
24
24
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
25
25
|
|
|
26
26
|
export interface DiscoveryModelEntry {
|
|
@@ -215,10 +215,7 @@ async function fetchRawModels(
|
|
|
215
215
|
.filter((m) => m.id);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function classifyLocally(
|
|
219
|
-
raw: RawUpstreamModel[],
|
|
220
|
-
cfg: ProxyConfig,
|
|
221
|
-
): Discovery {
|
|
218
|
+
function classifyLocally(raw: RawUpstreamModel[], cfg: ProxyConfig): Discovery {
|
|
222
219
|
const excludes = cfg.discoveryExcludes;
|
|
223
220
|
const builtinByName = new Map<string, DiscoveryBuiltinProvider>();
|
|
224
221
|
const customPool: DiscoveryCustomEntry[] = [];
|
package/src/log.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// Tiny logger that prefixes all messages with the extension tag.
|
|
2
2
|
// Uses console.* directly — pi pipes stdout/stderr into its log sink.
|
|
3
|
+
//
|
|
4
|
+
// While an interactive overlay is open, console output corrupts the TUI (it
|
|
5
|
+
// prints over the box and forces a redraw below it — stacking headers). The
|
|
6
|
+
// overlay code wraps its lifetime in setLogQuiet(true) to mute us; user-facing
|
|
7
|
+
// results are surfaced inside the overlay instead.
|
|
3
8
|
|
|
4
9
|
const TAG = "[pi-cliproxyapi]";
|
|
5
10
|
|
|
11
|
+
let quiet = false;
|
|
12
|
+
|
|
13
|
+
/** Mute/unmute all console output (used around interactive overlays). */
|
|
14
|
+
export function setLogQuiet(v: boolean): void {
|
|
15
|
+
quiet = v;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export const log = {
|
|
7
19
|
info(...args: unknown[]): void {
|
|
8
|
-
console.log(TAG, ...args);
|
|
20
|
+
if (!quiet) console.log(TAG, ...args);
|
|
9
21
|
},
|
|
10
22
|
warn(...args: unknown[]): void {
|
|
11
|
-
console.warn(TAG, ...args);
|
|
23
|
+
if (!quiet) console.warn(TAG, ...args);
|
|
12
24
|
},
|
|
13
25
|
error(...args: unknown[]): void {
|
|
14
|
-
console.error(TAG, ...args);
|
|
26
|
+
if (!quiet) console.error(TAG, ...args);
|
|
15
27
|
},
|
|
16
28
|
debug(...args: unknown[]): void {
|
|
17
|
-
if (process.env.PI_CLIPROXYAPI_DEBUG)
|
|
29
|
+
if (!quiet && process.env.PI_CLIPROXYAPI_DEBUG)
|
|
30
|
+
console.log(TAG, "[debug]", ...args);
|
|
18
31
|
},
|
|
19
32
|
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// /cliproxy hub — one overlay with Models / Usage / Diagnostics tabs and a
|
|
2
|
+
// shared action bar. Replaces the old per-command overlays (-refresh, -usage,
|
|
3
|
+
// -doctor) with global keys inside a single surface.
|
|
4
|
+
//
|
|
5
|
+
// global keys: [ ] / 1 2 3 switch tab \u00b7 r refresh \u00b7 e setup \u00b7 s save \u00b7 q close
|
|
6
|
+
// per-view keys: see each view's footerHint()
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ExtensionAPI,
|
|
10
|
+
ExtensionCommandContext,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
type Component,
|
|
14
|
+
getKeybindings,
|
|
15
|
+
matchesKey,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
|
|
18
|
+
import { applyAll } from "../apply.ts";
|
|
19
|
+
import type { ProxyConfig } from "../config.ts";
|
|
20
|
+
import { loadConfig, resolveConfigValue, saveConfig } from "../config.ts";
|
|
21
|
+
import type { Discovery } from "../fetch-models.ts";
|
|
22
|
+
import { fetchDiscovery } from "../fetch-models.ts";
|
|
23
|
+
import { clearUsageCache } from "../fetch-usage.ts";
|
|
24
|
+
import { frame } from "../ui-frame.ts";
|
|
25
|
+
import type { OverlayTui, Theme } from "../ui-picker/types.ts";
|
|
26
|
+
import { runSetup } from "../ui-setup.ts";
|
|
27
|
+
import { ruleLine, statusHeader, tabBar } from "./shell.ts";
|
|
28
|
+
import type { HubView } from "./types.ts";
|
|
29
|
+
import { buildDiagnosticsView } from "./view-diagnostics.ts";
|
|
30
|
+
import { buildModelsView, type ModelsView } from "./view-models.ts";
|
|
31
|
+
import { buildUsageView, type UsageView } from "./view-usage.ts";
|
|
32
|
+
|
|
33
|
+
export interface HubDeps {
|
|
34
|
+
pi: ExtensionAPI;
|
|
35
|
+
ctx: ExtensionCommandContext;
|
|
36
|
+
tui: OverlayTui;
|
|
37
|
+
theme: Theme;
|
|
38
|
+
cfg: ProxyConfig; // mutable draft owned by the hub
|
|
39
|
+
discovery: Discovery;
|
|
40
|
+
done: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildHub(
|
|
44
|
+
deps: HubDeps,
|
|
45
|
+
): Component & { handleInput(data: string): void } {
|
|
46
|
+
const { pi, ctx, tui, theme, cfg, done } = deps;
|
|
47
|
+
let discovery = deps.discovery;
|
|
48
|
+
let dirty = false; // unsaved config changes
|
|
49
|
+
let flash = ""; // transient status message (save/refresh/setup feedback)
|
|
50
|
+
|
|
51
|
+
const models: ModelsView = buildModelsView({
|
|
52
|
+
tui,
|
|
53
|
+
theme,
|
|
54
|
+
ctx,
|
|
55
|
+
cfg,
|
|
56
|
+
getDiscovery: () => discovery,
|
|
57
|
+
onChange: () => {
|
|
58
|
+
dirty = true;
|
|
59
|
+
flash = "";
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const usage: UsageView = buildUsageView({ tui, theme, cfg });
|
|
63
|
+
const diagnostics: HubView = buildDiagnosticsView({
|
|
64
|
+
theme,
|
|
65
|
+
cfg,
|
|
66
|
+
getDiscovery: () => discovery,
|
|
67
|
+
});
|
|
68
|
+
const views: HubView[] = [models, usage, diagnostics];
|
|
69
|
+
let activeIdx = 0;
|
|
70
|
+
const active = (): HubView => views[activeIdx]!;
|
|
71
|
+
|
|
72
|
+
const switchTab = (idx: number): void => {
|
|
73
|
+
if (idx < 0 || idx >= views.length || idx === activeIdx) return;
|
|
74
|
+
activeIdx = idx;
|
|
75
|
+
active().onActivate?.();
|
|
76
|
+
tui.requestRender();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const save = (): void => {
|
|
80
|
+
flash = theme.fg("dim", "saving\u2026");
|
|
81
|
+
saveConfig(cfg);
|
|
82
|
+
tui.requestRender();
|
|
83
|
+
void applyAll(pi, cfg, discovery)
|
|
84
|
+
.then((rep) => {
|
|
85
|
+
dirty = false;
|
|
86
|
+
flash = theme.fg(
|
|
87
|
+
"success",
|
|
88
|
+
`\u2713 settings saved \u00b7 ${rep.registered.length} registered, ${rep.skipped.length} skipped`,
|
|
89
|
+
);
|
|
90
|
+
tui.requestRender();
|
|
91
|
+
})
|
|
92
|
+
.catch((e: unknown) => {
|
|
93
|
+
flash = theme.fg("error", `save failed: ${(e as Error).message}`);
|
|
94
|
+
tui.requestRender();
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const refresh = async (): Promise<void> => {
|
|
99
|
+
flash = theme.fg("dim", "refreshing\u2026");
|
|
100
|
+
tui.requestRender();
|
|
101
|
+
try {
|
|
102
|
+
const key = resolveConfigValue(cfg.proxy.apiKey);
|
|
103
|
+
discovery = await fetchDiscovery(cfg, key);
|
|
104
|
+
clearUsageCache();
|
|
105
|
+
models.rebuild();
|
|
106
|
+
usage.reload();
|
|
107
|
+
const rep = await applyAll(pi, cfg, discovery);
|
|
108
|
+
flash = theme.fg(
|
|
109
|
+
"success",
|
|
110
|
+
`\u2713 refreshed \u00b7 ${rep.registered.length} providers (${discovery.source})`,
|
|
111
|
+
);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
flash = theme.fg("error", `refresh failed: ${(e as Error).message}`);
|
|
114
|
+
}
|
|
115
|
+
tui.requestRender();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const setup = async (): Promise<void> => {
|
|
119
|
+
const ok = await runSetup(ctx, true);
|
|
120
|
+
if (!ok) {
|
|
121
|
+
tui.requestRender();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Pull fresh credentials but keep the user's in-hub model edits.
|
|
125
|
+
const reloaded = loadConfig();
|
|
126
|
+
cfg.proxy = { ...cfg.proxy, ...reloaded.proxy };
|
|
127
|
+
await refresh();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ----- chrome ------------------------------------------------------------
|
|
131
|
+
const statusParts = (): string[] => {
|
|
132
|
+
const ep = cfg.proxy.endpoint || "(unset)";
|
|
133
|
+
const epShort = ep.length > 40 ? `\u2026${ep.slice(-39)}` : ep;
|
|
134
|
+
const keyOk = Boolean(resolveConfigValue(cfg.proxy.apiKey));
|
|
135
|
+
let provCount = 0;
|
|
136
|
+
let modelCount = 0;
|
|
137
|
+
for (const p of Object.values(cfg.builtinProviders)) {
|
|
138
|
+
if (p.enabled && p.models.length > 0) provCount++;
|
|
139
|
+
modelCount += p.models.length;
|
|
140
|
+
}
|
|
141
|
+
for (const p of Object.values(cfg.customProviders)) {
|
|
142
|
+
if (p.models.length > 0) provCount++;
|
|
143
|
+
modelCount += p.models.length;
|
|
144
|
+
}
|
|
145
|
+
const state = flash
|
|
146
|
+
? flash
|
|
147
|
+
: dirty
|
|
148
|
+
? theme.fg("warning", "\u25cf unsaved \u2014 press s to save")
|
|
149
|
+
: theme.fg("dim", "\u2713 saved");
|
|
150
|
+
return [
|
|
151
|
+
`${theme.fg("dim", "endpoint")} ${epShort}`,
|
|
152
|
+
`${theme.fg("dim", "key")} ${keyOk ? theme.fg("success", "\u2713") : theme.fg("error", "\u2717")}`,
|
|
153
|
+
`${theme.fg("dim", "providers")} ${provCount}`,
|
|
154
|
+
`${theme.fg("dim", "models")} ${modelCount}`,
|
|
155
|
+
state,
|
|
156
|
+
];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const render = (width: number): string[] => {
|
|
160
|
+
const totalRows = tui.rows ?? 40;
|
|
161
|
+
// frame() adds a top + bottom border, so the lines we hand it must total
|
|
162
|
+
// `frameTotal - 2`. Keep frameTotal within the overlay's 94% budget
|
|
163
|
+
// (proven range 16\u201338) so the box never overflows and gets clipped
|
|
164
|
+
// \u2014 clipping is what made the top unreachable and shifted the window.
|
|
165
|
+
const frameTotal = Math.max(16, Math.min(totalRows - 6, 38));
|
|
166
|
+
const bodyRows = frameTotal - 2;
|
|
167
|
+
const inner = Math.max(72, width - 2);
|
|
168
|
+
const viewBodyH = Math.max(6, bodyRows - 3); // minus status + tab + rule
|
|
169
|
+
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
lines.push(statusHeader(theme, statusParts(), inner));
|
|
172
|
+
lines.push(
|
|
173
|
+
tabBar(
|
|
174
|
+
theme,
|
|
175
|
+
views.map((v) => ({ id: v.id, label: v.label })),
|
|
176
|
+
activeIdx,
|
|
177
|
+
inner,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
lines.push(ruleLine(theme, inner));
|
|
181
|
+
lines.push(...active().render(inner, viewBodyH));
|
|
182
|
+
|
|
183
|
+
return frame(theme, {
|
|
184
|
+
width,
|
|
185
|
+
title: " /cliproxy ",
|
|
186
|
+
lines,
|
|
187
|
+
footer: {
|
|
188
|
+
hint: active().footerHint(),
|
|
189
|
+
badge:
|
|
190
|
+
" [ ] tab \u00b7 r refresh \u00b7 e setup \u00b7 s save \u00b7 q close ",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// ----- input -------------------------------------------------------------
|
|
196
|
+
const globalInput = (data: string): boolean | Promise<boolean> => {
|
|
197
|
+
const kb = getKeybindings();
|
|
198
|
+
if (
|
|
199
|
+
kb.matches(data, "tui.select.cancel") ||
|
|
200
|
+
matchesKey(data, "q") ||
|
|
201
|
+
matchesKey(data, "escape")
|
|
202
|
+
) {
|
|
203
|
+
done();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (matchesKey(data, "]") || matchesKey(data, "tab")) {
|
|
207
|
+
switchTab((activeIdx + 1) % views.length);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (matchesKey(data, "[") || matchesKey(data, "shift+tab")) {
|
|
211
|
+
switchTab((activeIdx - 1 + views.length) % views.length);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (matchesKey(data, "1")) {
|
|
215
|
+
switchTab(0);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (matchesKey(data, "2")) {
|
|
219
|
+
switchTab(1);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (matchesKey(data, "3")) {
|
|
223
|
+
switchTab(2);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (matchesKey(data, "r")) return refresh().then(() => true);
|
|
227
|
+
if (matchesKey(data, "e")) return setup().then(() => true);
|
|
228
|
+
if (matchesKey(data, "s")) {
|
|
229
|
+
save();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
render,
|
|
237
|
+
invalidate(): void {
|
|
238
|
+
/* stateless */
|
|
239
|
+
},
|
|
240
|
+
handleInput(data: string): void {
|
|
241
|
+
const handled = active().handleInput(data);
|
|
242
|
+
if (handled instanceof Promise) {
|
|
243
|
+
flash = "";
|
|
244
|
+
void handled.then((h) => {
|
|
245
|
+
if (h) tui.requestRender();
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (handled) {
|
|
250
|
+
flash = ""; // editing/navigating clears stale save feedback
|
|
251
|
+
tui.requestRender();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const g = globalInput(data);
|
|
255
|
+
if (g instanceof Promise) {
|
|
256
|
+
void g.then((h) => {
|
|
257
|
+
if (h) tui.requestRender();
|
|
258
|
+
});
|
|
259
|
+
} else if (g) {
|
|
260
|
+
tui.requestRender();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|