@tiens.nguyen/gu-cli 1.0.755 → 1.0.757

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
@@ -289,26 +289,38 @@ export function barNotice({ token, publicKeyB64, mode, now = new Date(), attempt
289
289
  if (check.ok) {
290
290
  const p = check.payload;
291
291
  const ended = p.paidUntil && Date.parse(p.paidUntil) <= now.getTime();
292
- if (ended) return { kind: "plan-ended", text: `Your gu Pro plan ended on ${shortDate(p.paidUntil)} — renew at ${pricingUrl}` };
292
+ // SHORT AND CALM (#292: "make it simpler … seems so serious"): the fact, then the one thing to
293
+ // do. `urgent` is what the bar colours by — only these two need the user to act.
294
+ if (ended) return { kind: "plan-ended", urgent: true, text: `Pro ended ${shortDate(p.paidUntil)} · renew at ${pricingUrl}` };
293
295
  if (Number.isInteger(p.activeDevices) && p.activeDevices > p.deviceLimit) {
294
296
  return {
295
297
  kind: "over-limit",
298
+ urgent: true,
296
299
  // PLAIN WORDS (asked: "make it simple to understand"). It read "gu is active on 4 devices —
297
300
  // Free covers 1", which leaves the reader to work out that "active" means "used gu in the
298
301
  // last 30 days" and "covers" means "the plan allows". Kept short: the bar drops it entirely
299
302
  // when it does not fit the terminal's width.
300
- text: `${planName(p.plan)} allows ${computers(p.deviceLimit)} — you've used gu on ${p.activeDevices} this month`,
303
+ text: `${planName(p.plan)}: ${computers(p.deviceLimit)} · used on ${p.activeDevices} this month`,
301
304
  };
302
305
  }
303
306
  const age = now.getTime() - Date.parse(p.issuedAt);
304
307
  if (age <= STALE_AFTER_DAYS * 86_400_000) return null;
305
308
  } else if (mode === "client" && !attempted) {
306
309
  return null; // Client mode fetches one by itself; do not complain before it has tried.
310
+ } else if (mode === "expert") {
311
+ // NO TOKEN on a Client Expert machine (#292): not "old" — there is none — and not said twice.
312
+ // Startup already prints the linking steps, and the first turn asks for the token in place, so
313
+ // the bar repeating it was the same message in two places (reported with a screenshot).
314
+ return null;
315
+ } else {
316
+ // Hosted client, its automatic fetch has failed: the bar is the only place this is said.
317
+ return { kind: "missing", urgent: false, text: "gu isn't signed in · run gu login" };
307
318
  }
308
319
 
320
+ // ROUTINE, so not an alarm (#292): a token getting old only means updates will stop one day.
309
321
  return mode === "expert"
310
- ? { kind: "stale", text: `Get a fresh gu token at ${accountUrl} to keep getting updates` }
311
- : { kind: "stale", text: "Sign in to keep getting gu updates — run gu login" };
322
+ ? { kind: "stale", urgent: false, text: `gu token is old · get a new one at ${accountUrl}` }
323
+ : { kind: "stale", urgent: false, text: "gu token is old · run gu login" };
312
324
  }
313
325
 
314
326
  /** "Free plan" / "Pro plan" — the name a person uses. */
@@ -12950,8 +12950,6 @@ def run_agent_chat(cfg):
12950
12950
  task_with_hint = _label_failed_images(task_with_hint, _img_failed)
12951
12951
  _log(f"attached {len(_turn_images)} image(s) to the question"
12952
12952
  + (f", {len(_img_failed)} could not be opened" if _img_failed else ""))
12953
- if _turn_images:
12954
- _emit({"type": "step", "text": f"Sending {len(_turn_images)} image(s) with the question"})
12955
12953
  for _lbl, _why in _img_failed:
12956
12954
  _emit({"type": "step", "text": f"⚠ {_lbl} not sent — {_why}"})
12957
12955
  else:
package/gu-repl.mjs CHANGED
@@ -128,7 +128,7 @@ import { unapprovedProjectServers } from "./mcp-command.mjs";
128
128
  import { readHooks } from "./hooks-config.mjs";
129
129
  import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
130
130
  import { codingChoiceTags, coderDisplayName, coderWhere, whereOfActiveCoder } from "./coding-choice-tags.mjs";
131
- import { IMAGE_NEEDS_CLAUDE, clipboardImageToFile, fitImageNotice, createImageStore, imagePasteKey, imagePathFromPaste, isClaudeCoderUrl, isImagePasteKey, storeImageFile } from "./image-paste.mjs";
131
+ import { IMAGE_NEEDS_CLAUDE, clipboardImageToFile, fitImageNotice, imageMarkersForDisplay, createImageStore, imagePasteKey, imagePathFromPaste, isClaudeCoderUrl, isImagePasteKey, storeImageFile } from "./image-paste.mjs";
132
132
  import {
133
133
  isDoubleClick,
134
134
  clickRun,
@@ -2474,7 +2474,10 @@ function frozenZone() {
2474
2474
  // it whole → the condition above lets the agents count (or nothing) have the row instead.
2475
2475
  const len = visibleLen(tokenNotice.text);
2476
2476
  const from = width - 1 - len;
2477
- rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") + yellow(tokenNotice.text), width - 1);
2477
+ // Yellow only when the user must act (plan ended, over the computer limit); an old token is
2478
+ // routine and reads in the rule's own dim grey (#292 — the yellow line "seems so serious").
2479
+ const paint = tokenNotice.urgent ? yellow : dim;
2480
+ rows[sep] = clipVisible(dim("\u2500".repeat(Math.max(0, from - 1)) + " ") + paint(tokenNotice.text), width - 1);
2478
2481
  } else if (agentsIndicator(otherAgents)) {
2479
2482
  // …and when there is no update to offer, the same row carries the agents count (task #195).
2480
2483
  // THREE THINGS CANNOT SHARE ONE ROW, so they are ranked by how time-critical they are:
@@ -9205,9 +9208,15 @@ async function localScaffoldState() {
9205
9208
  /** Print the startup scaffold line — deferred while a turn owns the transcript. */
9206
9209
  function announceScaffold() {
9207
9210
  if (!scaffoldNotice || following) return;
9208
- const { kind, text } = scaffoldNotice;
9211
+ const { kind, text, steps } = scaffoldNotice;
9209
9212
  scaffoldNotice = null;
9210
- console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow("⚠ " + text) : red("✗ " + text)));
9213
+ if (kind === "link") {
9214
+ // A setup step, said calmly (#292): the headline, then the steps on their own lines.
9215
+ console.log(GUTTER + yellow("! " + text));
9216
+ for (const s of steps ?? []) console.log(GUTTER + " " + dim(s));
9217
+ } else {
9218
+ console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow("⚠ " + text) : red("✗ " + text)));
9219
+ }
9211
9220
  if (barPinned()) paintBar();
9212
9221
  }
9213
9222
 
@@ -11423,7 +11432,8 @@ async function main() {
11423
11432
  // against the ">>" row. Unlike the live block's separator, this one belongs to the
11424
11433
  // TRANSCRIPT: the echo is written exactly once per submitted line and never repainted,
11425
11434
  // so there is nothing here for a blank to accumulate against.
11426
- process.stdout.write(PROMPT_PAST + line + "\n\n");
11435
+ // As typed, not as sent: "[Image #1]", never the file path the agent needs (#51).
11436
+ process.stdout.write(PROMPT_PAST + imageMarkersForDisplay(line) + "\n\n");
11427
11437
  }
11428
11438
  // ESC AT ANY QUESTION LANDS HERE (see PromptCancelled). Every command below is wrapped as
11429
11439
  // one block rather than each flow catching for itself, because the request was to be able to
package/image-paste.mjs CHANGED
@@ -214,6 +214,15 @@ export function clipboardImageToFile({ home = homedir(), platform = process.plat
214
214
  return { path: dest };
215
215
  }
216
216
 
217
+ /**
218
+ * A sent line as the USER should see it again: "[Image #1: /Users/…/x.png]" → "[Image #1]".
219
+ * The file path is for the agent, which reads the image from it; on screen it is noise (the echo
220
+ * of the question showed it). Claude Code shows the placeholder the same way.
221
+ */
222
+ export function imageMarkersForDisplay(text) {
223
+ return String(text ?? "").replace(/\[Image #(\d+): [^\]\n]+\]/g, "[Image #$1]");
224
+ }
225
+
217
226
  /** Placeholders shown in the input, and the files behind them. */
218
227
  export function createImageStore() {
219
228
  const store = new Map(); // token -> absolute path
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.755",
3
+ "version": "1.0.757",
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",
@@ -1890,6 +1890,22 @@ export function scaffoldStartupNotice({ local = null, refresh = null, private: p
1890
1890
  }
1891
1891
  if (refresh.ok) return { kind: "ok", text: `scaffold fetched from the gu service (v${refresh.version})` };
1892
1892
  const why = String(refresh.error ?? "unknown error").replace(/\.$/, "");
1893
+ // NOT LINKED YET (#292): the one "error" a new Client Expert machine always shows, and it is a
1894
+ // setup step, not a failure — gu asks for the token before the first turn anyway. It used to be
1895
+ // one red sentence ("× scaffold: … Please run `gu token` — copy … then: gu token …") wrapping
1896
+ // mid-word across three rows; now a calm headline and two numbered steps. The account address is
1897
+ // taken from the API's own text, so it follows the site the API was set up for.
1898
+ if (/isn't linked to your gu account/i.test(why)) {
1899
+ const url = (/(https?:\/\/[^\s)]+\/account)/.exec(why)?.[1] ?? "https://gu.getmarsic.cc/account").replace(/^https?:\/\//, "");
1900
+ return {
1901
+ kind: "link",
1902
+ text: "This computer isn't linked to your gu account yet.",
1903
+ steps: [
1904
+ `1. Copy your gu CLI token from ${url}`,
1905
+ "2. Run: gu token <your token> (or just ask something — gu will ask for it)",
1906
+ ],
1907
+ };
1908
+ }
1893
1909
  return refresh.kept !== undefined
1894
1910
  ? { kind: "warn", text: `scaffold: couldn't fetch — ${why}. Still using v${refresh.kept}.` }
1895
1911
  : { kind: "err", text: `scaffold: ${why}.` };