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.
@@ -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` plus confirmation to remove one permanently.
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, account.email || `account-${account.id.slice(0, 6)}`)
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.snapshot.warning
176
- ? this.theme.fg("warning", oneLine(this.snapshot.warning))
177
- : this.theme.fg("muted", `${active}/${accounts.length} active · ${this.snapshot.modelCount} models`);
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
- lines.push(truncateToWidth(`${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")}`, width, ""));
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, label) => {
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
- export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
73
- const entries: FactoryApiKeyEntry[] = [];
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 = loadFactoryApiKeys();
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.snapshot.warning
208
- ? this.theme.fg("warning", oneLine(this.snapshot.warning))
209
- : this.theme.fg("muted", `${ready} · ${this.snapshot.modelCount} models · Droid ${this.snapshot.version}`);
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
- lines.push(
244
- truncateToWidth(
245
- `${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", "esc")} ${this.theme.fg("dim", "close")}`,
246
- width,
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 = credentials.map((credential) => {
96
- const id = factoryCredentialId(credential.secret);
97
- const runtime = runtimeById.get(id);
98
- return {
99
- id,
100
- label: credential.label,
101
- record: cache.records.find((record) => record.id === id),
102
- cooldownSeconds: runtime?.cooldownSeconds,
103
- lastUsedAt: runtime?.lastUsedAt,
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(ctx, initial, async (force) => {
199
- await refreshLimits(ctx, force);
200
- return dashboardSnapshot(ctx);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",