@tiens.nguyen/gu-cli 1.0.737 → 1.0.739
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/cli-token.mjs +34 -1
- package/gu-repl.mjs +25 -6
- package/package.json +1 -1
package/cli-token.mjs
CHANGED
|
@@ -88,7 +88,9 @@ export function verifyCliToken(token, publicKeyB64) {
|
|
|
88
88
|
!(p.activeDevices === null || (Number.isInteger(p.activeDevices) && p.activeDevices >= 0)) ||
|
|
89
89
|
!(Number.isInteger(p.deviceLimit) && p.deviceLimit > 0) ||
|
|
90
90
|
// Missing means OFF — tokens issued before the switch existed (#238) are in CLIs right now.
|
|
91
|
-
!(p.enforce === undefined || typeof p.enforce === "boolean")
|
|
91
|
+
!(p.enforce === undefined || typeof p.enforce === "boolean") ||
|
|
92
|
+
// Missing is every token before #267; present, it must be an address.
|
|
93
|
+
!(p.email === undefined || isEmail(p.email))
|
|
92
94
|
) {
|
|
93
95
|
return { ok: false, why: "bad-payload" };
|
|
94
96
|
}
|
|
@@ -315,6 +317,37 @@ const planName = (plan) => (plan === "pro" ? "Pro plan" : "Free plan");
|
|
|
315
317
|
const computers = (n) => `${n} ${n === 1 ? "computer" : "computers"}`;
|
|
316
318
|
|
|
317
319
|
/** What `gu token` prints about a valid token — plain lines, no colour, for the command to style. */
|
|
320
|
+
/** A plain address, as the API signs it (#267). */
|
|
321
|
+
export function isEmail(v) {
|
|
322
|
+
return typeof v === "string" && v.trim().length <= 254 && /^[^\s@]+@[^\s@]+$/.test(v.trim());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* THE BANNER'S ACCOUNT LINE, under the worker key (#267): which account, and whether it is Pro.
|
|
327
|
+
* · email — the signed token's (private mode's only source: its local API knows no email), else
|
|
328
|
+
* the one the API reports (client mode);
|
|
329
|
+
* · plan — from the VERIFIED token, judged NOW: a token signed while Pro can outlive the
|
|
330
|
+
* subscription, and the line must not keep saying Pro after it ended.
|
|
331
|
+
* → { email, plan, pro } — empty strings when unknown; the caller prints nothing for nothing.
|
|
332
|
+
*/
|
|
333
|
+
export function accountLine({ payload = null, settingsEmail = "", now = new Date() } = {}) {
|
|
334
|
+
const email = (payload && isEmail(payload.email) ? payload.email.trim() : "") || (isEmail(settingsEmail) ? settingsEmail.trim() : "");
|
|
335
|
+
let plan = "";
|
|
336
|
+
let pro = false;
|
|
337
|
+
if (payload) {
|
|
338
|
+
const until = Date.parse(payload.paidUntil ?? "");
|
|
339
|
+
if (payload.plan === "pro" && Number.isFinite(until) && until > now.getTime()) {
|
|
340
|
+
plan = `Pro until ${shortDate(payload.paidUntil)}`;
|
|
341
|
+
pro = true;
|
|
342
|
+
} else if (payload.paidUntil) {
|
|
343
|
+
plan = `Free — Pro ended ${shortDate(payload.paidUntil)}`;
|
|
344
|
+
} else {
|
|
345
|
+
plan = "Free";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { email, plan, pro };
|
|
349
|
+
}
|
|
350
|
+
|
|
318
351
|
export function tokenSummary(payload, now = new Date()) {
|
|
319
352
|
const lines = [];
|
|
320
353
|
if (payload.plan === "pro" && payload.paidUntil && Date.parse(payload.paidUntil) > now.getTime()) {
|
package/gu-repl.mjs
CHANGED
|
@@ -6415,6 +6415,19 @@ function listJsonFiles(dir) {
|
|
|
6415
6415
|
|
|
6416
6416
|
/** The email of the account this worker key belongs to, or "" (older API, none on record, offline). */
|
|
6417
6417
|
let accountEmailPromise = null;
|
|
6418
|
+
/** The banner's account line (#267): the saved gu token, verified offline, plus the API's email. */
|
|
6419
|
+
async function bannerAccount(settingsEmail = "") {
|
|
6420
|
+
try {
|
|
6421
|
+
const ct = await import("./cli-token.mjs");
|
|
6422
|
+
const pk = ct.publicKey();
|
|
6423
|
+
const state = pk ? await ct.readState(join(homedir(), ".gonext")) : { token: "" };
|
|
6424
|
+
const check = state.token ? ct.verifyCliToken(state.token, pk) : { ok: false };
|
|
6425
|
+
return ct.accountLine({ payload: check.ok ? check.payload : null, settingsEmail });
|
|
6426
|
+
} catch {
|
|
6427
|
+
return { email: settingsEmail || "", plan: "", pro: false };
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6418
6431
|
async function fetchAccountEmail() {
|
|
6419
6432
|
const s = await fetchWorkerSettings();
|
|
6420
6433
|
const e = typeof s?.userEmail === "string" ? s.userEmail.trim() : "";
|
|
@@ -10514,8 +10527,9 @@ async function main() {
|
|
|
10514
10527
|
// Show which models will answer, straight from user settings (also validates auth).
|
|
10515
10528
|
try {
|
|
10516
10529
|
const probe = await probeWithRecovery();
|
|
10517
|
-
// WHICH ACCOUNT, shown under the key in the banner
|
|
10518
|
-
//
|
|
10530
|
+
// WHICH ACCOUNT, shown under the key in the banner. Asked for now and read when the banner is
|
|
10531
|
+
// drawn, so startup never waits on it. Private mode's local API knows no email — there it comes
|
|
10532
|
+
// from the signed gu token instead (bannerAccount, #267).
|
|
10519
10533
|
accountEmailPromise = isClientExpert() ? Promise.resolve("") : fetchAccountEmail();
|
|
10520
10534
|
// A NEW gu SESSION: where does its scaffold come from? (task #246) Your file ("loaded") or the
|
|
10521
10535
|
// gu service ("fetched" — the refresh the session then reuses). Background: never delays startup.
|
|
@@ -10641,11 +10655,16 @@ async function main() {
|
|
|
10641
10655
|
// shown when it is actually routing, and never the credentials — host and user only.
|
|
10642
10656
|
if (bannerProxy) statusRows.push(lbl("proxy") + bannerProxy);
|
|
10643
10657
|
statusRows.push(dim(`key ${workerKey.slice(0, 8)}…`));
|
|
10644
|
-
// Under the key
|
|
10645
|
-
// to the wrong account is obvious at a glance.
|
|
10646
|
-
//
|
|
10658
|
+
// Under the key: the account this machine signs in as, and its PLAN (#267) — so a machine
|
|
10659
|
+
// paired to the wrong account, or a lapsed Pro, is obvious at a glance. Both from the VERIFIED
|
|
10660
|
+
// gu token (no network call), which is the only source in private mode; client mode falls back
|
|
10661
|
+
// to the email its API reports. Nothing known → no line: absent beats guessed.
|
|
10647
10662
|
const accountEmail = await (accountEmailPromise ?? Promise.resolve(""));
|
|
10648
|
-
|
|
10663
|
+
const acct = await bannerAccount(accountEmail);
|
|
10664
|
+
if (acct.email || acct.plan) {
|
|
10665
|
+
const plan = acct.plan ? (acct.pro ? accent(acct.plan) : dim(acct.plan)) : "";
|
|
10666
|
+
statusRows.push(acct.email ? dim(`account ${acct.email}`) + (plan ? dim(" · ") + plan : "") : dim("plan ") + plan);
|
|
10667
|
+
}
|
|
10649
10668
|
console.log(renderBanner(statusRows));
|
|
10650
10669
|
}
|
|
10651
10670
|
// Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.739",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|