shariq-pi-extensions 0.2.24 → 0.2.26
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/extensions/antigravity-provider/README.md +1 -1
- package/extensions/antigravity-provider/antigravity/dashboard.ts +26 -5
- package/extensions/antigravity-provider/antigravity/index.ts +1 -3
- package/extensions/factory-provider/API_KEYS.md +1 -1
- package/extensions/factory-provider/README.md +1 -1
- package/extensions/factory-provider/factory/api-keys.ts +63 -7
- package/extensions/factory-provider/factory/dashboard.ts +67 -21
- package/extensions/factory-provider/index.ts +45 -16
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ Local persistent Pi provider for Google Antigravity-compatible models.
|
|
|
10
10
|
|
|
11
11
|
The provider includes an IPv4 OAuth token-exchange fallback for Node environments where the default request fails. Its curated catalog follows current Antigravity model identifiers and runtime behavior.
|
|
12
12
|
|
|
13
|
-
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions. A missing account file triggers one-time migration of an existing Pi OAuth credential; after that, the valid account file is authoritative, so removing an account cannot be undone by a stale credential in `auth.json`. Requests select the least recently used eligible account, skip disabled/cooling/exhausted accounts, refresh expiring OAuth tokens, and rotate to another account when an auth, rate, quota, or capacity failure occurs before response streaming begins. Cached per-model remaining quota and reset times guide selection; `/antigravity` refreshes the authoritative catalog, uses `d` to reversibly enable or disable an account, and uses `x`
|
|
13
|
+
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions. A missing account file triggers one-time migration of an existing Pi OAuth credential; after that, the valid account file is authoritative, so removing an account cannot be undone by a stale credential in `auth.json`. Requests select the least recently used eligible account, skip disabled/cooling/exhausted accounts, refresh expiring OAuth tokens, and rotate to another account when an auth, rate, quota, or capacity failure occurs before response streaming begins. Cached per-model remaining quota and reset times guide selection; `/antigravity` refreshes the authoritative catalog, uses `d` to reversibly enable or disable an account, and uses `x` twice to confirm and remove one permanently; Escape cancels an armed removal.
|
|
14
14
|
|
|
15
15
|
Current public model IDs:
|
|
16
16
|
- `antigravity/gemini-3.7-flash`
|
|
@@ -71,6 +71,7 @@ function compactGroup(label: string, entries: AntigravityQuotaEntry[]) {
|
|
|
71
71
|
export class AntigravityDashboard implements Component {
|
|
72
72
|
private selected = 0;
|
|
73
73
|
private refreshing = false;
|
|
74
|
+
private pendingRemoval: { id: string; label: string } | undefined;
|
|
74
75
|
private closed = false;
|
|
75
76
|
private readonly tui: TUI;
|
|
76
77
|
private readonly theme: Theme;
|
|
@@ -132,21 +133,36 @@ export class AntigravityDashboard implements Component {
|
|
|
132
133
|
handleInput(data: string) {
|
|
133
134
|
const accounts = this.snapshot.accounts;
|
|
134
135
|
if (this.keys.matches(data, "tui.select.cancel")) {
|
|
136
|
+
if (this.pendingRemoval) {
|
|
137
|
+
this.pendingRemoval = undefined;
|
|
138
|
+
this.tui.requestRender();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
135
141
|
this.closed = true;
|
|
136
142
|
this.done();
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
145
|
if (this.keys.matches(data, "tui.select.up") || data === "k") {
|
|
146
|
+
this.pendingRemoval = undefined;
|
|
140
147
|
if (accounts.length) this.selected = (this.selected - 1 + accounts.length) % accounts.length;
|
|
141
148
|
} else if (this.keys.matches(data, "tui.select.down") || data === "j") {
|
|
149
|
+
this.pendingRemoval = undefined;
|
|
142
150
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
143
151
|
} else if (data === "r") {
|
|
152
|
+
this.pendingRemoval = undefined;
|
|
144
153
|
this.startRefresh(true);
|
|
145
154
|
} else if ((data === "d" || data === "x") && accounts[this.selected] && !this.refreshing) {
|
|
146
155
|
const account = accounts[this.selected]!;
|
|
156
|
+
const label = account.email || `account-${account.id.slice(0, 6)}`;
|
|
157
|
+
if (data === "x" && this.pendingRemoval?.id !== account.id) {
|
|
158
|
+
this.pendingRemoval = { id: account.id, label };
|
|
159
|
+
this.tui.requestRender();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.pendingRemoval = undefined;
|
|
147
163
|
this.refreshing = true;
|
|
148
164
|
const action = data === "x"
|
|
149
|
-
? this.removeAccount(account.id,
|
|
165
|
+
? this.removeAccount(account.id, label)
|
|
150
166
|
: this.toggleAccount(account.id, account.disabled === true);
|
|
151
167
|
void action
|
|
152
168
|
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
@@ -172,9 +188,11 @@ export class AntigravityDashboard implements Component {
|
|
|
172
188
|
const active = accounts.filter((account) => account.active).length;
|
|
173
189
|
const right = this.refreshing
|
|
174
190
|
? this.theme.fg("warning", "refreshing…")
|
|
175
|
-
: this.
|
|
176
|
-
? this.theme.fg("warning", oneLine(this.
|
|
177
|
-
: this.
|
|
191
|
+
: this.pendingRemoval
|
|
192
|
+
? this.theme.fg("warning", `press x again to remove ${oneLine(this.pendingRemoval.label)}`)
|
|
193
|
+
: this.snapshot.warning
|
|
194
|
+
? this.theme.fg("warning", oneLine(this.snapshot.warning))
|
|
195
|
+
: this.theme.fg("muted", `${active}/${accounts.length} active · ${this.snapshot.modelCount} models`);
|
|
178
196
|
const title = ` ${this.theme.fg("accent", this.theme.bold("◆ ANTIGRAVITY"))} ${this.theme.fg("dim", `· ${this.snapshot.authentication}`)}`;
|
|
179
197
|
const lines = width >= 64
|
|
180
198
|
? [joinSides(title, `${right} `, width)]
|
|
@@ -194,7 +212,10 @@ export class AntigravityDashboard implements Component {
|
|
|
194
212
|
for (let row = 0; row < bodyHeight; row++) lines.push(this.theme.fg("border", "│") + padLine(list[row] ?? "", inner) + this.theme.fg("border", "│"));
|
|
195
213
|
}
|
|
196
214
|
lines.push(frameBottom(this.theme, width));
|
|
197
|
-
|
|
215
|
+
const controls = this.pendingRemoval
|
|
216
|
+
? `${this.theme.fg("warning", " x")} ${this.theme.fg("warning", "confirm permanent removal")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "cancel")}`
|
|
217
|
+
: `${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`;
|
|
218
|
+
lines.push(truncateToWidth(controls, width, ""));
|
|
198
219
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
199
220
|
}
|
|
200
221
|
|
|
@@ -135,9 +135,7 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
135
135
|
if (enabled) await refreshAntigravityQuotas({ force: true, signal: ctx.signal });
|
|
136
136
|
return dashboardSnapshot(ctx);
|
|
137
137
|
},
|
|
138
|
-
async (id
|
|
139
|
-
const confirmed = await ctx.ui.confirm("Remove Antigravity account?", `Permanently remove ${label} from this Pi installation?`);
|
|
140
|
-
if (!confirmed) return dashboardSnapshot(ctx);
|
|
138
|
+
async (id) => {
|
|
141
139
|
removeAntigravityAccount(id);
|
|
142
140
|
await reconcileStoredCredential(ctx);
|
|
143
141
|
return dashboardSnapshot(ctx);
|
|
@@ -14,4 +14,4 @@ Alternatively choose Pi's top-level **Use an API key** authentication method to
|
|
|
14
14
|
|
|
15
15
|
Use `/logout` to remove the active Factory authentication selection. The configured key file is intentionally not deleted by logout.
|
|
16
16
|
|
|
17
|
-
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. For a smoke test, use the current `factory/kimi-k3` model.
|
|
17
|
+
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. File-backed keys remain visible when disabled: select one and press `d` to enable/disable it, or press `x` twice to confirm permanent removal from `api-keys.json`; Escape cancels an armed removal. Environment-provided keys are read-only. For a smoke test, use the current `factory/kimi-k3` model.
|
|
@@ -59,7 +59,7 @@ There are no `-oauth` or `-api-key` model duplicates. The user-curated catalog e
|
|
|
59
59
|
/factory
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, and press Escape to close.
|
|
62
|
+
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, use `d` to reversibly enable or disable a rotating file-backed key, press `x` twice to confirm and remove one permanently, and press Escape to cancel an armed removal or close the dashboard. Environment variables and single credentials managed by Pi auth remain read-only in this dashboard.
|
|
63
63
|
|
|
64
64
|
Usage is never estimated from Pi token counts: Factory's API already applies model multipliers and cache-hit discounts. Cached records refresh at most every 15 minutes during normal sessions and after Factory runs; the manual command is the explicit force-refresh path.
|
|
65
65
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
5
5
|
import { FACTORY_API_BASE_URL, FACTORY_STATE_DIR } from "./constants.ts";
|
|
@@ -25,6 +25,10 @@ export type FactoryApiKeyEntry = {
|
|
|
25
25
|
disabled?: boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type FactoryApiKeySourceEntry = FactoryApiKeyEntry & {
|
|
29
|
+
source: "environment" | "file";
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
type FactoryApiKeyConfig = {
|
|
29
33
|
keys?: Array<string | Partial<FactoryApiKeyEntry>>;
|
|
30
34
|
};
|
|
@@ -69,19 +73,19 @@ export function parseFactoryApiKeyFile(raw: string): { entries: FactoryApiKeyEnt
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
const entries:
|
|
76
|
+
function configuredFactoryApiKeys(): FactoryApiKeySourceEntry[] {
|
|
77
|
+
const entries: FactoryApiKeySourceEntry[] = [];
|
|
74
78
|
const envKeys = (process.env.FACTORY_API_KEYS || process.env.FACTORY_API_KEY || "")
|
|
75
79
|
.split(/[\n,]+/)
|
|
76
80
|
.map((key) => key.trim())
|
|
77
81
|
.filter(Boolean);
|
|
78
|
-
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key }));
|
|
82
|
+
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key, source: "environment" }));
|
|
79
83
|
|
|
80
84
|
lastConfigurationWarning = undefined;
|
|
81
85
|
if (existsSync(FACTORY_API_KEYS_PATH)) {
|
|
82
86
|
try {
|
|
83
87
|
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
84
|
-
entries.push(...parsed.entries);
|
|
88
|
+
entries.push(...parsed.entries.map((entry) => ({ ...entry, source: "file" as const })));
|
|
85
89
|
lastConfigurationWarning = parsed.warning;
|
|
86
90
|
} catch {
|
|
87
91
|
lastConfigurationWarning = "Factory rotating-key configuration could not be read.";
|
|
@@ -90,13 +94,63 @@ export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
|
90
94
|
|
|
91
95
|
const seen = new Set<string>();
|
|
92
96
|
return entries.filter((entry) => {
|
|
93
|
-
if (entry.disabled) return false;
|
|
94
97
|
if (seen.has(entry.key)) return false;
|
|
95
98
|
seen.add(entry.key);
|
|
96
99
|
return true;
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
104
|
+
return configuredFactoryApiKeys().filter((entry) => !entry.disabled);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function editableFactoryApiKeyEntries(): FactoryApiKeyEntry[] {
|
|
108
|
+
if (!existsSync(FACTORY_API_KEYS_PATH)) return [];
|
|
109
|
+
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
110
|
+
if (parsed.warning) throw new Error(parsed.warning);
|
|
111
|
+
return parsed.entries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function saveFactoryApiKeyEntries(entries: FactoryApiKeyEntry[]) {
|
|
115
|
+
mkdirSync(FACTORY_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
116
|
+
chmodSync(FACTORY_STATE_DIR, 0o700);
|
|
117
|
+
const temporary = `${FACTORY_API_KEYS_PATH}.${process.pid}.tmp`;
|
|
118
|
+
writeFileSync(temporary, `${JSON.stringify({ keys: entries }, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
renameSync(temporary, FACTORY_API_KEYS_PATH);
|
|
120
|
+
chmodSync(FACTORY_API_KEYS_PATH, 0o600);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function updateFactoryApiKeyEntries(
|
|
124
|
+
entries: FactoryApiKeyEntry[],
|
|
125
|
+
id: string,
|
|
126
|
+
action: { kind: "enabled"; enabled: boolean } | { kind: "remove" },
|
|
127
|
+
) {
|
|
128
|
+
let changed = false;
|
|
129
|
+
const next = entries.flatMap((entry): FactoryApiKeyEntry[] => {
|
|
130
|
+
if (keyId(entry) !== id) return [entry];
|
|
131
|
+
changed = true;
|
|
132
|
+
if (action.kind === "remove") return [];
|
|
133
|
+
return [{ ...entry, disabled: !action.enabled }];
|
|
134
|
+
});
|
|
135
|
+
return { entries: next, changed };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function setFactoryApiKeyEnabled(id: string, enabled: boolean) {
|
|
139
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "enabled", enabled });
|
|
140
|
+
if (!result.changed) return false;
|
|
141
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function removeFactoryApiKey(id: string) {
|
|
146
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "remove" });
|
|
147
|
+
if (!result.changed) return false;
|
|
148
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
149
|
+
state.delete(id);
|
|
150
|
+
organizationIdCache.delete(id);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
export function sortFactoryApiKeysByLastUsed(
|
|
101
155
|
entries: FactoryApiKeyEntry[],
|
|
102
156
|
lastUsed = (entry: FactoryApiKeyEntry) => state.get(keyId(entry))?.lastUsedAt,
|
|
@@ -395,7 +449,7 @@ export function streamSimpleUnifiedFactoryResponses(model: any, context: any, op
|
|
|
395
449
|
}
|
|
396
450
|
|
|
397
451
|
export function factoryApiKeyStatus() {
|
|
398
|
-
const keys =
|
|
452
|
+
const keys = configuredFactoryApiKeys();
|
|
399
453
|
const now = Date.now();
|
|
400
454
|
return {
|
|
401
455
|
configured: keys.length,
|
|
@@ -408,6 +462,8 @@ export function factoryApiKeyStatus() {
|
|
|
408
462
|
id: keyId(entry),
|
|
409
463
|
label: entry.label,
|
|
410
464
|
key: maskKey(entry.key),
|
|
465
|
+
disabled: Boolean(entry.disabled),
|
|
466
|
+
editable: entry.source === "file",
|
|
411
467
|
cooldownSeconds: runtime?.cooldownUntil && runtime.cooldownUntil > now ? Math.ceil((runtime.cooldownUntil - now) / 1000) : 0,
|
|
412
468
|
lastError: runtime?.lastError ? runtime.lastError.slice(0, 200) : undefined,
|
|
413
469
|
lastUsedAt: runtime?.lastUsedAt ? new Date(runtime.lastUsedAt).toISOString() : undefined,
|
|
@@ -26,6 +26,8 @@ import { factoryWarmTheme } from "./warm-theme.ts";
|
|
|
26
26
|
export interface FactoryDashboardAccount {
|
|
27
27
|
id: string;
|
|
28
28
|
label: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
editable?: boolean;
|
|
29
31
|
record?: FactoryLimitRecord;
|
|
30
32
|
cooldownSeconds?: number;
|
|
31
33
|
lastUsedAt?: string;
|
|
@@ -96,6 +98,7 @@ function compactPool(
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function accountState(account: FactoryDashboardAccount) {
|
|
101
|
+
if (account.disabled) return "disabled";
|
|
99
102
|
if (account.cooldownSeconds && account.cooldownSeconds > 0) return "cooldown";
|
|
100
103
|
if (account.record?.error) return "refresh error";
|
|
101
104
|
const observedAt = account.record?.fetchedAt ?? Date.now();
|
|
@@ -116,12 +119,15 @@ function stateColor(state: string) {
|
|
|
116
119
|
export class FactoryDashboard implements Component {
|
|
117
120
|
private selected = 0;
|
|
118
121
|
private refreshing = false;
|
|
122
|
+
private pendingRemoval: { id: string; label: string } | undefined;
|
|
119
123
|
private closed = false;
|
|
120
124
|
private readonly tui: TUI;
|
|
121
125
|
private readonly theme: Theme;
|
|
122
126
|
private readonly keys: KeybindingsManager;
|
|
123
127
|
private snapshot: FactoryDashboardSnapshot;
|
|
124
128
|
private readonly refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
129
|
+
private readonly toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
130
|
+
private readonly removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>;
|
|
125
131
|
private readonly done: () => void;
|
|
126
132
|
|
|
127
133
|
constructor(
|
|
@@ -130,6 +136,8 @@ export class FactoryDashboard implements Component {
|
|
|
130
136
|
keys: KeybindingsManager,
|
|
131
137
|
snapshot: FactoryDashboardSnapshot,
|
|
132
138
|
refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
139
|
+
toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
140
|
+
removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
133
141
|
done: () => void,
|
|
134
142
|
) {
|
|
135
143
|
this.tui = tui;
|
|
@@ -137,6 +145,8 @@ export class FactoryDashboard implements Component {
|
|
|
137
145
|
this.keys = keys;
|
|
138
146
|
this.snapshot = snapshot;
|
|
139
147
|
this.refreshData = refreshData;
|
|
148
|
+
this.toggleAccount = toggleAccount;
|
|
149
|
+
this.removeAccount = removeAccount;
|
|
140
150
|
this.done = done;
|
|
141
151
|
}
|
|
142
152
|
|
|
@@ -145,17 +155,7 @@ export class FactoryDashboard implements Component {
|
|
|
145
155
|
this.refreshing = true;
|
|
146
156
|
this.tui.requestRender();
|
|
147
157
|
void this.refreshData(force)
|
|
148
|
-
.then((snapshot) =>
|
|
149
|
-
if (this.closed) return;
|
|
150
|
-
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
151
|
-
this.snapshot = snapshot;
|
|
152
|
-
const nextIndex = selectedId
|
|
153
|
-
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
154
|
-
: -1;
|
|
155
|
-
this.selected = nextIndex >= 0
|
|
156
|
-
? nextIndex
|
|
157
|
-
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
158
|
-
})
|
|
158
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
159
159
|
.catch((error) => {
|
|
160
160
|
if (this.closed) return;
|
|
161
161
|
this.snapshot = {
|
|
@@ -170,6 +170,18 @@ export class FactoryDashboard implements Component {
|
|
|
170
170
|
});
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
private replaceSnapshot(snapshot: FactoryDashboardSnapshot) {
|
|
174
|
+
if (this.closed) return;
|
|
175
|
+
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
176
|
+
this.snapshot = snapshot;
|
|
177
|
+
const nextIndex = selectedId
|
|
178
|
+
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
179
|
+
: -1;
|
|
180
|
+
this.selected = nextIndex >= 0
|
|
181
|
+
? nextIndex
|
|
182
|
+
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
183
|
+
}
|
|
184
|
+
|
|
173
185
|
dispose() {
|
|
174
186
|
this.closed = true;
|
|
175
187
|
}
|
|
@@ -179,18 +191,49 @@ export class FactoryDashboard implements Component {
|
|
|
179
191
|
handleInput(data: string) {
|
|
180
192
|
const accounts = this.snapshot.accounts;
|
|
181
193
|
if (this.keys.matches(data, "tui.select.cancel")) {
|
|
194
|
+
if (this.pendingRemoval) {
|
|
195
|
+
this.pendingRemoval = undefined;
|
|
196
|
+
this.tui.requestRender();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
182
199
|
this.closed = true;
|
|
183
200
|
this.done();
|
|
184
201
|
return;
|
|
185
202
|
}
|
|
186
203
|
if (this.keys.matches(data, "tui.select.up") || data === "k") {
|
|
204
|
+
this.pendingRemoval = undefined;
|
|
187
205
|
if (accounts.length) {
|
|
188
206
|
this.selected = (this.selected - 1 + accounts.length) % accounts.length;
|
|
189
207
|
}
|
|
190
208
|
} else if (this.keys.matches(data, "tui.select.down") || data === "j") {
|
|
209
|
+
this.pendingRemoval = undefined;
|
|
191
210
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
192
211
|
} else if (data === "r") {
|
|
212
|
+
this.pendingRemoval = undefined;
|
|
193
213
|
this.startRefresh(true);
|
|
214
|
+
} else if ((data === "d" || data === "x") && accounts[this.selected]?.editable && !this.refreshing) {
|
|
215
|
+
const account = accounts[this.selected]!;
|
|
216
|
+
if (data === "x" && this.pendingRemoval?.id !== account.id) {
|
|
217
|
+
this.pendingRemoval = { id: account.id, label: account.label };
|
|
218
|
+
this.tui.requestRender();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.pendingRemoval = undefined;
|
|
222
|
+
this.refreshing = true;
|
|
223
|
+
const action = data === "x"
|
|
224
|
+
? this.removeAccount(account.id, account.label)
|
|
225
|
+
: this.toggleAccount(account.id, account.disabled === true);
|
|
226
|
+
void action
|
|
227
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
228
|
+
.catch((error) => {
|
|
229
|
+
if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
|
|
230
|
+
})
|
|
231
|
+
.finally(() => {
|
|
232
|
+
if (!this.closed) {
|
|
233
|
+
this.refreshing = false;
|
|
234
|
+
this.tui.requestRender();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
194
237
|
}
|
|
195
238
|
this.tui.requestRender();
|
|
196
239
|
}
|
|
@@ -204,9 +247,11 @@ export class FactoryDashboard implements Component {
|
|
|
204
247
|
const ready = `${this.snapshot.active}/${this.snapshot.configured} active`;
|
|
205
248
|
const headerRight = this.refreshing
|
|
206
249
|
? this.theme.fg("warning", "refreshing…")
|
|
207
|
-
: this.
|
|
208
|
-
? this.theme.fg("warning", oneLine(this.
|
|
209
|
-
: this.
|
|
250
|
+
: this.pendingRemoval
|
|
251
|
+
? this.theme.fg("warning", `press x again to remove ${oneLine(this.pendingRemoval.label)}`)
|
|
252
|
+
: this.snapshot.warning
|
|
253
|
+
? this.theme.fg("warning", oneLine(this.snapshot.warning))
|
|
254
|
+
: this.theme.fg("muted", `${ready} · ${this.snapshot.modelCount} models · Droid ${this.snapshot.version}`);
|
|
210
255
|
const title = ` ${this.theme.fg("accent", this.theme.bold("◆ FACTORY"))} ${this.theme.fg("dim", `· ${this.snapshot.authentication}`)}`;
|
|
211
256
|
const lines = width >= 60
|
|
212
257
|
? [joinSides(title, `${headerRight} `, width)]
|
|
@@ -240,13 +285,10 @@ export class FactoryDashboard implements Component {
|
|
|
240
285
|
}
|
|
241
286
|
}
|
|
242
287
|
lines.push(frameBottom(this.theme, width));
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"",
|
|
248
|
-
),
|
|
249
|
-
);
|
|
288
|
+
const controls = this.pendingRemoval
|
|
289
|
+
? `${this.theme.fg("warning", " x")} ${this.theme.fg("warning", "confirm permanent removal")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "cancel")}`
|
|
290
|
+
: `${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`;
|
|
291
|
+
lines.push(truncateToWidth(controls, width, ""));
|
|
250
292
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
251
293
|
}
|
|
252
294
|
|
|
@@ -341,6 +383,8 @@ export async function openFactoryDashboard(
|
|
|
341
383
|
ctx: ExtensionContext,
|
|
342
384
|
initial: FactoryDashboardSnapshot,
|
|
343
385
|
refresh: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
386
|
+
toggle: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
387
|
+
remove: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
344
388
|
) {
|
|
345
389
|
await ctx.ui.custom<void>(
|
|
346
390
|
(tui, theme, keys, done) => {
|
|
@@ -350,6 +394,8 @@ export async function openFactoryDashboard(
|
|
|
350
394
|
keys,
|
|
351
395
|
initial,
|
|
352
396
|
refresh,
|
|
397
|
+
toggle,
|
|
398
|
+
remove,
|
|
353
399
|
() => done(undefined),
|
|
354
400
|
);
|
|
355
401
|
queueMicrotask(() => dashboard.startRefresh(false));
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
FACTORY_API_KEY_FILE_SENTINEL,
|
|
12
12
|
factoryApiKeyStatus,
|
|
13
13
|
loadFactoryApiKeys,
|
|
14
|
+
removeFactoryApiKey,
|
|
15
|
+
setFactoryApiKeyEnabled,
|
|
14
16
|
streamSimpleUnifiedFactoryResponses,
|
|
15
17
|
} from "./factory/api-keys.ts";
|
|
16
18
|
import {
|
|
@@ -88,21 +90,35 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
88
90
|
ctx: ExtensionContext,
|
|
89
91
|
): Promise<FactoryDashboardSnapshot> => {
|
|
90
92
|
const credentials = await limitCredentials(ctx);
|
|
93
|
+
const resolved = await ctx.modelRegistry.getProviderAuth(PROVIDER_ID);
|
|
94
|
+
const fileBacked = resolved?.auth.apiKey?.trim() === FACTORY_API_KEY_FILE_SENTINEL;
|
|
91
95
|
const auth = ctx.modelRegistry.getProviderAuthStatus(PROVIDER_ID);
|
|
92
96
|
const apiKeys = factoryApiKeyStatus();
|
|
93
97
|
const cache = loadFactoryLimitCache();
|
|
94
98
|
const runtimeById = new Map(apiKeys.keys.map((key) => [key.id, key]));
|
|
95
|
-
const accounts =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const accounts = fileBacked
|
|
100
|
+
? apiKeys.keys.map((key) => ({
|
|
101
|
+
id: key.id,
|
|
102
|
+
label: key.label,
|
|
103
|
+
disabled: key.disabled,
|
|
104
|
+
editable: key.editable,
|
|
105
|
+
record: cache.records.find((record) => record.id === key.id),
|
|
106
|
+
cooldownSeconds: key.cooldownSeconds,
|
|
107
|
+
lastUsedAt: key.lastUsedAt,
|
|
108
|
+
}))
|
|
109
|
+
: credentials.map((credential) => {
|
|
110
|
+
const id = factoryCredentialId(credential.secret);
|
|
111
|
+
const runtime = runtimeById.get(id);
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
label: credential.label,
|
|
115
|
+
disabled: false,
|
|
116
|
+
editable: false,
|
|
117
|
+
record: cache.records.find((record) => record.id === id),
|
|
118
|
+
cooldownSeconds: runtime?.cooldownSeconds,
|
|
119
|
+
lastUsedAt: runtime?.lastUsedAt,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
106
122
|
return {
|
|
107
123
|
version: clientVersion,
|
|
108
124
|
modelCount: models.length,
|
|
@@ -110,7 +126,7 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
110
126
|
? auth.label || auth.source || "configured"
|
|
111
127
|
: "not configured",
|
|
112
128
|
configured: accounts.length,
|
|
113
|
-
active: accounts.filter((account) => !account.cooldownSeconds).length,
|
|
129
|
+
active: accounts.filter((account) => !account.disabled && !account.cooldownSeconds).length,
|
|
114
130
|
warning: apiKeys.warning,
|
|
115
131
|
accounts,
|
|
116
132
|
};
|
|
@@ -195,10 +211,23 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
195
211
|
description: "Open Factory account, rotation, and usage dashboard",
|
|
196
212
|
handler: async (_args, ctx) => {
|
|
197
213
|
const initial = await dashboardSnapshot(ctx);
|
|
198
|
-
await openFactoryDashboard(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
await openFactoryDashboard(
|
|
215
|
+
ctx,
|
|
216
|
+
initial,
|
|
217
|
+
async (force) => {
|
|
218
|
+
await refreshLimits(ctx, force);
|
|
219
|
+
return dashboardSnapshot(ctx);
|
|
220
|
+
},
|
|
221
|
+
async (id, enabled) => {
|
|
222
|
+
if (!setFactoryApiKeyEnabled(id, enabled)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
223
|
+
if (enabled) await refreshLimits(ctx, true);
|
|
224
|
+
return dashboardSnapshot(ctx);
|
|
225
|
+
},
|
|
226
|
+
async (id) => {
|
|
227
|
+
if (!removeFactoryApiKey(id)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
228
|
+
return dashboardSnapshot(ctx);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
202
231
|
},
|
|
203
232
|
});
|
|
204
233
|
}
|