pi-freeflow 1.23.0 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.23.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Cline logins no longer accept the same account twice. Two logins for one account share a single free quota, so the second one added nothing while making it look like you had more capacity — signing in with an account you already saved now says which slot has it and leaves your logins untouched. If a set of logins already contains the same account twice, `/freeflow cline accounts` marks the later one `— same account as [slot]` so you can tell them apart and remove the spare.
8
+
3
9
  ## 1.23.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -239,6 +239,8 @@ Rotating per-account promo, direct only (never through the relay pool). Sign in
239
239
 
240
240
  Add as many logins as you want — each one gets its own slot (`default`, `slot-2`, `slot-3`, …) and a request that hits a limit on one rolls straight to the next. To sign in with a different email, sign out of Cline in that browser first (`/freeflow cline signout` prints the link), or use a private window so your current session stays put.
241
241
 
242
+ Each account can only be saved once: an extra login for an account you already have shares the same free quota, so it is refused rather than saved. If a pool already holds two logins for one account (saved before this check), `/freeflow cline accounts` marks the later one `— same account as [default]`, and `/freeflow cline logout <slot>` drops it.
243
+
242
244
  | Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
243
245
  | :--- | :--- | :--- | :--- | :--- | :--- |
244
246
  | `cline-free/deepseek-v4.1-flash` | DeepSeek | **1M** (1.000.000) | **384K** (384.000) | `low/high/max` | ✅ |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.23.0",
4
+ "version": "1.23.1",
5
5
  "description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
6
6
  "main": "extensions/index.ts",
7
7
  "types": "src/index.ts",
@@ -238,11 +238,48 @@ function markActiveSlotOnDisk(slot: string): void {
238
238
  savePool(live);
239
239
  }
240
240
 
241
+ /**
242
+ * Identity of a Cline login. Two slots holding the same account share one free
243
+ * quota, so rotating between them buys nothing — callers use this to detect
244
+ * that instead of saving a duplicate.
245
+ *
246
+ * accountId (WorkOS user id) is authoritative; email is the readable fallback.
247
+ * When neither side carries an identity — a legacy `clp_` key slot has none —
248
+ * only an identical bearer proves the same account.
249
+ */
250
+ function sameClineAccount(account: ClineAccount, identity: { token?: string; accountId?: string; email?: string }): boolean {
251
+ const idA = account.accountId?.trim();
252
+ const idB = identity.accountId?.trim();
253
+ if (idA && idB) return idA === idB;
254
+ const mailA = account.email?.trim().toLowerCase();
255
+ const mailB = identity.email?.trim().toLowerCase();
256
+ if (mailA && mailB) return mailA === mailB;
257
+ const token = identity.token;
258
+ return typeof token === "string" && token.length > 0 && account.token === token;
259
+ }
260
+
261
+ /**
262
+ * The other slot already holding this account, or null when it is new.
263
+ * `exceptSlot` skips the slot being written, so re-logging into an existing
264
+ * slot is never treated as a duplicate.
265
+ */
266
+ export function findClineAccountSlot(
267
+ pool: ClinePoolState,
268
+ identity: { token?: string; accountId?: string; email?: string },
269
+ exceptSlot?: string,
270
+ ): string | null {
271
+ const hit = pool.accounts.find(
272
+ (a) => a.slot !== exceptSlot && sameClineAccount(a, identity),
273
+ );
274
+ return hit ? hit.slot : null;
275
+ }
276
+
241
277
  /**
242
278
  * Save (or replace) one login slot. The token must carry the `workos:` prefix.
243
279
  * Throws on bad input — the message never echoes the token.
244
280
  * Extras carry the device-login grant (refresh token, expiry, identity);
245
281
  * legacy key slots keep calling with two args and load untouched.
282
+ * Refuses an account that another slot already holds.
246
283
  */
247
284
  export function addAccount(
248
285
  slot: string,
@@ -255,6 +292,11 @@ export function addAccount(
255
292
  if (!token.startsWith("workos:") && !isWorkosJwt(token) && !token.startsWith("clp_")) throw new Error("Cline token must be a workos: login grant or a clp_ API key");
256
293
  const pool = loadPool();
257
294
  const existing = pool.accounts.find((a) => a.slot === cleanSlot);
295
+ const duplicate = findClineAccountSlot(pool, { token, accountId: extras?.accountId, email: extras?.email }, cleanSlot);
296
+ if (duplicate) {
297
+ const who = extras?.email?.trim() || existing?.email?.trim() || "this account";
298
+ throw new Error(`That Cline account (${who}) is already saved as [${duplicate}] — log in with a different account, or use /freeflow cline logout ${duplicate} first`);
299
+ }
258
300
  if (existing) {
259
301
  existing.token = token;
260
302
  if (extras?.refreshToken) existing.refreshToken = extras.refreshToken;
package/src/commands.ts CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  formatRelayPickerItem,
59
59
  formatRelayStatusLabel,
60
60
  } from "./relay-state.ts";
61
- import { addAccount, loadPool, redactedToken, removeAccount } from "./cline-accounts.ts";
61
+ import { addAccount, findClineAccountSlot, loadPool, redactedToken, removeAccount } from "./cline-accounts.ts";
62
62
  import type { ClinePoolState } from "./cline-accounts.ts";
63
63
  import { CLINE_BROWSER_SIGNOUT_URL, pollDeviceToken, registerClineToken, startDeviceAuth, toApiKey } from "./cline-device-auth.ts";
64
64
  import { isCertError } from "./system-ca-fetch.ts";
@@ -369,7 +369,12 @@ function formatClineAccountLines(pool: ClinePoolState): string[] {
369
369
  const star = a.slot === pool.activeSlot ? "*" : " ";
370
370
  const who = a.email || a.accountId;
371
371
  const whoPart = who ? ` ${who}` : "";
372
- return `${star} [${idx + 1}] [${a.slot}]${whoPart} key ending ${redactedToken(a.token)}`;
372
+ // Two slots on one account share one quota, so rotation between them buys
373
+ // nothing. Mark the later one against the earlier slot it duplicates.
374
+ const earlier = { accounts: pool.accounts.slice(0, idx) };
375
+ const dup = findClineAccountSlot(earlier, a);
376
+ const dupPart = dup ? ` — same account as [${dup}]` : "";
377
+ return `${star} [${idx + 1}] [${a.slot}]${whoPart} key ending ${redactedToken(a.token)}${dupPart}`;
373
378
  });
374
379
  }
375
380
  /** User-safe one-line message for caught values (never echoes secrets). */
@@ -1475,6 +1480,15 @@ export function createCommandSpec(
1475
1480
  const deviceTokens = await pollDeviceToken(undefined, started.deviceCode, started.interval, { maxWaitMs: Math.max(60_000, started.expiresIn * 1000) });
1476
1481
  const creds = await registerClineToken(undefined, deviceTokens.accessToken, deviceTokens.refreshToken);
1477
1482
  const apiKey = toApiKey(creds.access);
1483
+ const dup = findClineAccountSlot(loadPool(), { token: apiKey, accountId: creds.accountId, email: creds.email }, slot);
1484
+ if (dup) {
1485
+ // Same account twice shares one free quota, so it would never add capacity.
1486
+ ctx.ui.notify(
1487
+ `That Cline account${creds.email ? ` (${creds.email})` : ""} is already saved as [${dup}] — nothing added.\nUse a different account: /freeflow cline signout shows the sign-out link. Or drop the existing one first: /freeflow cline logout ${dup}`,
1488
+ "warning",
1489
+ );
1490
+ return;
1491
+ }
1478
1492
  addAccount(slot, apiKey, {
1479
1493
  refreshToken: creds.refresh,
1480
1494
  expiresAt: creds.expires,