@tiens.nguyen/gu-cli 1.0.736 → 1.0.738

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 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 (client mode). Asked for now and read when
10518
- // the banner is drawn, so startup never waits on it; private mode has no web account to name.
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, in client mode: the account this machine signs in as — so a machine paired
10645
- // to the wrong account is obvious at a glance. Nothing when the API does not say (older API,
10646
- // or no email on record): an absent line is better than a guessed one.
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
- if (accountEmail) statusRows.push(dim(`account ${accountEmail}`));
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.736",
3
+ "version": "1.0.738",
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",