@tiens.nguyen/gu-cli 1.0.861 → 1.0.862

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/gu-repl.mjs CHANGED
@@ -532,6 +532,10 @@ const SPINNERS = [
532
532
  // green, cyan, blue, indigo, pink, amber).
533
533
  const WAIT_COLORS_256 = [120, 87, 111, 147, 218, 221];
534
534
  const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
535
+ // The thinking/waiting spinner + playful word (#216): plain white by default, the cycling
536
+ // WAIT_COLORS_256 colour only when this run was started with `gu --color`. White goes through
537
+ // paint(), so NO_COLOR / TERM=dumb still turn it off.
538
+ const waitPaint = (n, s) => (REPL_COLOR ? color256(n, s) : white(s));
535
539
  const orange = (s) => color256(208, s); // peak-token label (task #84)
536
540
  // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
537
541
  const fmtTokens = (n) => {
@@ -616,7 +620,7 @@ if (cliVerdict.kind === "run") {
616
620
  } else if (argv.includes("--help") || argv.includes("-h")) {
617
621
  console.log(
618
622
  "gu — terminal agent REPL for the current folder\n\n" +
619
- "Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
623
+ "Usage: gu [-v|--verbose] [--log] [--color] [--license <pro token>]\n" +
620
624
  " gu status # worker + a table of the running model servers\n" +
621
625
  " gu start|stop|restart|logs # manage the background worker\n" +
622
626
  " gu logs <port|pid> [-f] # tail a running model server's log\n" +
@@ -625,6 +629,8 @@ if (cliVerdict.kind === "run") {
625
629
  " -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
626
630
  " --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
627
631
  " stops and errors are logged. Ends when this gu closes.\n" +
632
+ " --color colour the thinking spinner and its playful word (cycling colours).\n" +
633
+ " Without it they are plain white.\n" +
628
634
  " --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
629
635
  " client / MLX host / private). Without it, a first install is\n" +
630
636
  " private by default — the whole stack on this machine.\n" +
@@ -649,6 +655,9 @@ const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
649
655
  // worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
650
656
  // run that is killed leaves a lease for a dead process, which is no lease.
651
657
  const REPL_LOG = argv.includes("--log");
658
+ // --color (#216): the thinking spinner and its playful word cycle through colours only on a run
659
+ // started with this flag; by default they are plain white.
660
+ const REPL_COLOR = argv.includes("--color");
652
661
  if (REPL_LOG) {
653
662
  const ll = await import("./log-level.mjs");
654
663
  await ll.takeLease().catch(() => {});
@@ -4265,8 +4274,8 @@ function pendingStatusRows() {
4265
4274
  const wc = WAIT_COLORS_256[Math.floor(now / 2500) % WAIT_COLORS_256.length];
4266
4275
  const secs = Math.max(0, Math.round((now - _pendingSince) / 1000));
4267
4276
  const line =
4268
- markLine(color256(wc, glyph)) +
4269
- color256(wc, `${_pendingLabel}…`) +
4277
+ markLine(waitPaint(wc, glyph)) +
4278
+ waitPaint(wc, `${_pendingLabel}…`) +
4270
4279
  (secs >= 2 ? dim(` (${secs}s)`) : "");
4271
4280
  return [clipVisible(line, width - 1)].concat(
4272
4281
  barPinned() || !process.stdout.isTTY ? [] : footerRows()
@@ -12458,7 +12467,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
12458
12467
  for (const wl of expLines) contentRows.push(clipRow(GUTTER + dim(wl)));
12459
12468
  for (let i = 0; i < WORD_GAP; i++) contentRows.push(""); // see WORD_GAP
12460
12469
  contentRows.push(
12461
- clipRow(`${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${outLabel}`)
12470
+ clipRow(`${markLine(waitPaint(wc, glyph))}${waitPaint(wc, `${thinkWord}…`)}${outLabel}`)
12462
12471
  );
12463
12472
  // PINNED → the block IS frozen rows; unpinned (a window too short to reserve any) there is
12464
12473
  // no zone, so it keeps the old in-place draw at the output position.
package/gu_browser.py CHANGED
@@ -291,12 +291,42 @@ def should_offer(now=None):
291
291
 
292
292
 
293
293
  def mark_offered():
294
+ """→ whether the marker was written (a failed write must not mean "offer again", #217)."""
294
295
  try:
295
296
  os.makedirs(os.path.dirname(_offered_path()), exist_ok=True)
296
297
  with open(_offered_path(), "w") as f:
297
298
  f.write(str(int(time.time())))
299
+ return True
298
300
  except OSError:
299
- pass
301
+ return False
302
+
303
+
304
+ def claim_offer(now=None):
305
+ """Atomically decide that THIS process may open the store page (#217): True for exactly one of
306
+ several gu processes racing, and only if it was not opened in the last OFFER_EVERY_S. False when
307
+ GU_NO_OPEN_APPS is set (tests) or when the marker cannot be written — never offer without a
308
+ record that we did."""
309
+ if os.environ.get("GU_NO_OPEN_APPS"):
310
+ return False
311
+ lock = _offered_path() + ".lock"
312
+ try:
313
+ os.makedirs(os.path.dirname(lock), exist_ok=True)
314
+ try:
315
+ if time.time() - os.path.getmtime(lock) > 60: # a crashed claimer's leftover
316
+ os.remove(lock)
317
+ except OSError:
318
+ pass
319
+ fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
320
+ except OSError:
321
+ return False # another process holds the claim right now (or no writable folder)
322
+ try:
323
+ os.close(fd)
324
+ return should_offer(now) and mark_offered()
325
+ finally:
326
+ try:
327
+ os.remove(lock)
328
+ except OSError:
329
+ pass
300
330
 
301
331
 
302
332
  def open_store_page(url=None, platform=None):
@@ -369,9 +399,8 @@ def ensure_extension(log=None, emit=None, wait_s=120, installs=None):
369
399
  installs = find_installs() if installs is None else installs
370
400
  state = diagnose(installs)
371
401
  opened = False
372
- if state == "missing" and should_offer():
373
- opened = open_store_page()
374
- mark_offered()
402
+ if state == "missing" and claim_offer():
403
+ opened = open_store_page() # claim_offer already recorded it: at most once a day, never per call
375
404
  steps = setup_steps(state, installs, opened)
376
405
  say = emit or (lambda _t: None)
377
406
  for line in steps:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.861",
3
+ "version": "1.0.862",
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",