pi-freeflow 1.22.4 → 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 +12 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/cline-accounts.ts +42 -0
- package/src/cline-device-auth.ts +10 -0
- package/src/commands.ts +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 1.23.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Adding more Cline logins is now easy. Every login gets its own slot (`default`, `slot-2`, `slot-3`, …) and requests roll across all of them, so a second or third account keeps you working when one hits its free daily limit. The sign-in prompt now shows the link to sign out of Cline in your browser (`/freeflow cline signout` prints it any time), which is what you need to approve the next account with a different email — or use a private window and keep the session you already have. `/freeflow cline accounts` shows each login's email so you can tell them apart.
|
|
14
|
+
|
|
3
15
|
## 1.22.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -177,6 +177,7 @@ The same command set works identically in OMP and Pi:
|
|
|
177
177
|
/freeflow cline login # Sign in through the browser, save to the per-user pool
|
|
178
178
|
/freeflow cline accounts # List saved Cline logins
|
|
179
179
|
/freeflow cline logout # Remove a saved Cline login
|
|
180
|
+
/freeflow cline signout # Link to sign out of Cline in your browser (to add another account)
|
|
180
181
|
/freeflow export [path] [--include-secrets] # Save the relay pool to a file (default freeflow-relays.json; passwords left out unless asked)
|
|
181
182
|
/freeflow import <path> [--merge|--replace] [--dry-run] # Load a relay pool from a file (merge is default; replace asks first; dry-run previews only)
|
|
182
183
|
```
|
|
@@ -236,6 +237,10 @@ Keyless access. Short aliases work for every row (the full ID is in parentheses)
|
|
|
236
237
|
|
|
237
238
|
Rotating per-account promo, direct only (never through the relay pool). Sign in through your browser with `/freeflow cline login`, manage with `/freeflow cline accounts` and `/freeflow cline logout`. Requests roll across saved logins when one hits its daily free limit.
|
|
238
239
|
|
|
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
|
+
|
|
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
|
+
|
|
239
244
|
| Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
|
|
240
245
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
241
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.
|
|
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",
|
package/src/cline-accounts.ts
CHANGED
|
@@ -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/cline-device-auth.ts
CHANGED
|
@@ -45,6 +45,16 @@ export const DEFAULT_WORKOS_BASE = "https://api.workos.com";
|
|
|
45
45
|
export const DEFAULT_API_BASE = "https://api.cline.bot";
|
|
46
46
|
export const WORKOS_TOKEN_PREFIX = "workos:";
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Cline's own sign-out route, for switching accounts in the browser. Verified
|
|
50
|
+
* live: it answers 302 -> https://app.cline.bot/dashboard. Cline ships no
|
|
51
|
+
* hosted sign-out page on its auth host (every /logout, /sign-out, /signout,
|
|
52
|
+
* /account path there is 404), so this app route is the one to point at.
|
|
53
|
+
* The device flow reuses whatever account the browser is signed in with, and
|
|
54
|
+
* a private window clears that without ending the current session.
|
|
55
|
+
*/
|
|
56
|
+
export const CLINE_BROWSER_SIGNOUT_URL = "https://api.cline.bot/api/v1/auth/logout";
|
|
57
|
+
|
|
48
58
|
/** Env override for the WorkOS client id (wins over the bundled default). */
|
|
49
59
|
export const CLINE_WORKOS_CLIENT_ID_ENV = "PI_FREEFLOW_CLINE_WORKOS_CLIENT_ID";
|
|
50
60
|
|
package/src/commands.ts
CHANGED
|
@@ -58,9 +58,9 @@ 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
|
-
import { pollDeviceToken, registerClineToken, startDeviceAuth, toApiKey } from "./cline-device-auth.ts";
|
|
63
|
+
import { CLINE_BROWSER_SIGNOUT_URL, pollDeviceToken, registerClineToken, startDeviceAuth, toApiKey } from "./cline-device-auth.ts";
|
|
64
64
|
import { isCertError } from "./system-ca-fetch.ts";
|
|
65
65
|
import type {
|
|
66
66
|
ExtensionAPI,
|
|
@@ -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
|
-
|
|
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). */
|
|
@@ -1463,7 +1468,7 @@ export function createCommandSpec(
|
|
|
1463
1468
|
}
|
|
1464
1469
|
const link = started.verificationUriComplete ?? started.verificationUri;
|
|
1465
1470
|
const minutes = Math.max(1, Math.round(started.expiresIn / 60));
|
|
1466
|
-
const loginLine = `Cline login [${slot}]\nOpen this link in your browser:\n${link}\nEnter code: ${started.userCode} (expires in ~${minutes} min)\nWaiting for approval — approve or cancel in the browser; this finishes on its own
|
|
1471
|
+
const loginLine = `Cline login [${slot}]\nOpen this link in your browser:\n${link}\nEnter code: ${started.userCode} (expires in ~${minutes} min)\nWaiting for approval — approve or cancel in the browser; this finishes on its own.\nDifferent account? Sign out first: ${CLINE_BROWSER_SIGNOUT_URL} (or use a private window)`;
|
|
1467
1472
|
ctx.ui.notify(loginLine, "info");
|
|
1468
1473
|
try { ctx.ui.setStatus("cline-login", `Cline login [${slot}] code ${started.userCode}`); } catch { }
|
|
1469
1474
|
const progress = setInterval(() => {
|
|
@@ -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,
|
|
@@ -1550,8 +1564,15 @@ export function createCommandSpec(
|
|
|
1550
1564
|
}
|
|
1551
1565
|
}
|
|
1552
1566
|
}
|
|
1567
|
+
} else if (action === "signout" || action === "sign-out" || action === "switch") {
|
|
1568
|
+
// Signing out is a browser action; it never touches saved logins here.
|
|
1569
|
+
const saved = loadPool().accounts.length;
|
|
1570
|
+
ctx.ui.notify(
|
|
1571
|
+
`Sign out of Cline in your browser to use a different account:\n${CLINE_BROWSER_SIGNOUT_URL}\nA private window works too, and keeps your current session.\nThen run /freeflow cline login again — each login gets its own slot (${saved ? `you have ${saved}; next is ` : "next is "}${nextClineSlot(loadPool())}).`,
|
|
1572
|
+
"info",
|
|
1573
|
+
);
|
|
1553
1574
|
} else {
|
|
1554
|
-
ctx.ui.notify("Usage: /freeflow cline login [--key] [slot] | /freeflow cline accounts | /freeflow cline logout [slot]", "warning");
|
|
1575
|
+
ctx.ui.notify("Usage: /freeflow cline login [--key] [slot] | /freeflow cline accounts | /freeflow cline logout [slot] | /freeflow cline signout", "warning");
|
|
1555
1576
|
}
|
|
1556
1577
|
} else if (sub === "export") {
|
|
1557
1578
|
const tokens = rest.trim() ? rest.trim().split(/\s+/) : [];
|