shariq-pi-extensions 0.2.24 → 0.2.25

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.
@@ -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` and confirm to remove it permanently from `api-keys.json`. 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, use `x` plus confirmation to remove one permanently, and press Escape to close. 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();
@@ -122,6 +125,8 @@ export class FactoryDashboard implements Component {
122
125
  private readonly keys: KeybindingsManager;
123
126
  private snapshot: FactoryDashboardSnapshot;
124
127
  private readonly refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>;
128
+ private readonly toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>;
129
+ private readonly removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>;
125
130
  private readonly done: () => void;
126
131
 
127
132
  constructor(
@@ -130,6 +135,8 @@ export class FactoryDashboard implements Component {
130
135
  keys: KeybindingsManager,
131
136
  snapshot: FactoryDashboardSnapshot,
132
137
  refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>,
138
+ toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
139
+ removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
133
140
  done: () => void,
134
141
  ) {
135
142
  this.tui = tui;
@@ -137,6 +144,8 @@ export class FactoryDashboard implements Component {
137
144
  this.keys = keys;
138
145
  this.snapshot = snapshot;
139
146
  this.refreshData = refreshData;
147
+ this.toggleAccount = toggleAccount;
148
+ this.removeAccount = removeAccount;
140
149
  this.done = done;
141
150
  }
142
151
 
@@ -145,17 +154,7 @@ export class FactoryDashboard implements Component {
145
154
  this.refreshing = true;
146
155
  this.tui.requestRender();
147
156
  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
- })
157
+ .then((snapshot) => this.replaceSnapshot(snapshot))
159
158
  .catch((error) => {
160
159
  if (this.closed) return;
161
160
  this.snapshot = {
@@ -170,6 +169,18 @@ export class FactoryDashboard implements Component {
170
169
  });
171
170
  }
172
171
 
172
+ private replaceSnapshot(snapshot: FactoryDashboardSnapshot) {
173
+ if (this.closed) return;
174
+ const selectedId = this.snapshot.accounts[this.selected]?.id;
175
+ this.snapshot = snapshot;
176
+ const nextIndex = selectedId
177
+ ? snapshot.accounts.findIndex((account) => account.id === selectedId)
178
+ : -1;
179
+ this.selected = nextIndex >= 0
180
+ ? nextIndex
181
+ : Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
182
+ }
183
+
173
184
  dispose() {
174
185
  this.closed = true;
175
186
  }
@@ -191,6 +202,23 @@ export class FactoryDashboard implements Component {
191
202
  if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
192
203
  } else if (data === "r") {
193
204
  this.startRefresh(true);
205
+ } else if ((data === "d" || data === "x") && accounts[this.selected]?.editable && !this.refreshing) {
206
+ const account = accounts[this.selected]!;
207
+ this.refreshing = true;
208
+ const action = data === "x"
209
+ ? this.removeAccount(account.id, account.label)
210
+ : this.toggleAccount(account.id, account.disabled === true);
211
+ void action
212
+ .then((snapshot) => this.replaceSnapshot(snapshot))
213
+ .catch((error) => {
214
+ if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
215
+ })
216
+ .finally(() => {
217
+ if (!this.closed) {
218
+ this.refreshing = false;
219
+ this.tui.requestRender();
220
+ }
221
+ });
194
222
  }
195
223
  this.tui.requestRender();
196
224
  }
@@ -242,7 +270,7 @@ export class FactoryDashboard implements Component {
242
270
  lines.push(frameBottom(this.theme, width));
243
271
  lines.push(
244
272
  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")}`,
273
+ `${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")}`,
246
274
  width,
247
275
  "",
248
276
  ),
@@ -341,6 +369,8 @@ export async function openFactoryDashboard(
341
369
  ctx: ExtensionContext,
342
370
  initial: FactoryDashboardSnapshot,
343
371
  refresh: (force: boolean) => Promise<FactoryDashboardSnapshot>,
372
+ toggle: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
373
+ remove: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
344
374
  ) {
345
375
  await ctx.ui.custom<void>(
346
376
  (tui, theme, keys, done) => {
@@ -350,6 +380,8 @@ export async function openFactoryDashboard(
350
380
  keys,
351
381
  initial,
352
382
  refresh,
383
+ toggle,
384
+ remove,
353
385
  () => done(undefined),
354
386
  );
355
387
  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,25 @@ 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, label) => {
227
+ const confirmed = await ctx.ui.confirm("Remove Factory account?", `Permanently remove ${label} from the rotating-key file?`);
228
+ if (!confirmed) return dashboardSnapshot(ctx);
229
+ if (!removeFactoryApiKey(id)) throw new Error("This Factory credential is not editable from the rotating-key file.");
230
+ return dashboardSnapshot(ctx);
231
+ },
232
+ );
202
233
  },
203
234
  });
204
235
  }
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.25",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",