gnhf 0.1.36 → 0.1.37

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.mjs +98 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -203,6 +203,7 @@ If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf as
203
203
  | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
204
204
  | `--current-branch` | Run on the current branch instead of creating a `gnhf/` branch | `false` |
205
205
  | `--push` | Push the current branch after each successful iteration | `false` |
206
+ | `--meteor-frequency <n>` | Set TUI meteor frequency from 0 to 5 (`0` disables meteors) | `3` |
206
207
  | `--version` | Show version | |
207
208
 
208
209
  ## Configuration
package/dist/cli.mjs CHANGED
@@ -17287,6 +17287,7 @@ const STAR_CHARS = [
17287
17287
  "°",
17288
17288
  "°"
17289
17289
  ];
17290
+ const MIN_METEOR_START_GAP_MS = 500;
17290
17291
  function generateStarField(width, height, density, seed) {
17291
17292
  const stars = [];
17292
17293
  let s = seed;
@@ -17321,6 +17322,47 @@ function getStarState(star, now) {
17321
17322
  if (t > .025) return "bright";
17322
17323
  return "dim";
17323
17324
  }
17325
+ function generateMeteorShower(width, height, count, seed) {
17326
+ if (width <= 0 || height <= 0 || count <= 0) return [];
17327
+ const meteors = [];
17328
+ let s = seed % 2147483647;
17329
+ if (s <= 0) s += 2147483646;
17330
+ const rand = () => {
17331
+ s = s * 16807 % 2147483647;
17332
+ return s / 2147483647;
17333
+ };
17334
+ for (let i = 0; i < count; i++) {
17335
+ const length = Math.min(height, 2 + (i + Math.floor(rand() * 6)) % 6);
17336
+ const xMax = Math.max(1, width - length);
17337
+ const yMin = Math.max(0, length - 1);
17338
+ const yMaxExclusive = Math.max(yMin + 1, Math.ceil(height * .75));
17339
+ const ySpan = Math.max(1, yMaxExclusive - yMin);
17340
+ const period = 16e3 + rand() * 2e4;
17341
+ const duration = count >= 8 ? 3e3 + rand() * 600 : 900 + rand() * 500;
17342
+ meteors.push({
17343
+ x: Math.floor(rand() * xMax),
17344
+ y: yMin + Math.floor(rand() * ySpan),
17345
+ length,
17346
+ phase: i === 0 ? 0 : i * (MIN_METEOR_START_GAP_MS + 80) + rand() * 60,
17347
+ period,
17348
+ duration
17349
+ });
17350
+ }
17351
+ return meteors;
17352
+ }
17353
+ function getMeteorTrail(meteor, now) {
17354
+ const cycleTime = ((now - meteor.phase) % meteor.period + meteor.period) % meteor.period;
17355
+ if (cycleTime >= meteor.duration) return [];
17356
+ const step = Math.floor(cycleTime / 120);
17357
+ const cells = [];
17358
+ for (let i = 0; i < meteor.length; i++) cells.push({
17359
+ x: meteor.x - step + i,
17360
+ y: meteor.y + step - i,
17361
+ state: i === 0 ? "bright" : "dim",
17362
+ char: "╱"
17363
+ });
17364
+ return cells;
17365
+ }
17324
17366
  //#endregion
17325
17367
  //#region src/utils/moon.ts
17326
17368
  const MOON_PHASES = [
@@ -17544,6 +17586,8 @@ const CONTENT_WIDTH = 63;
17544
17586
  const MAX_PROMPT_LINES = 3;
17545
17587
  const BASE_CONTENT_ROWS = 24;
17546
17588
  const STAR_DENSITY = .035;
17589
+ const DEFAULT_METEOR_FREQUENCY = 3;
17590
+ const METEOR_SEED_OFFSET = 101;
17547
17591
  const TICK_MS = 200;
17548
17592
  const MOONS_PER_ROW = 30;
17549
17593
  const MOON_PHASE_PERIOD = 1600;
@@ -17647,6 +17691,17 @@ function starStyle(state) {
17647
17691
  if (state === "dim") return "dim";
17648
17692
  return "normal";
17649
17693
  }
17694
+ function meteorCountForFrequency(frequency) {
17695
+ if (frequency <= 0) return 0;
17696
+ if (frequency === 1) return 1;
17697
+ if (frequency === 2) return 2;
17698
+ if (frequency === 3) return 4;
17699
+ if (frequency === 4) return 6;
17700
+ return 28;
17701
+ }
17702
+ function meteorsStartingBefore(meteors, rowOffset, maxStartRow) {
17703
+ return meteors.filter((meteor) => rowOffset + meteor.y < maxStartRow);
17704
+ }
17650
17705
  function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
17651
17706
  for (const star of stars) {
17652
17707
  if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
@@ -17663,15 +17718,28 @@ function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
17663
17718
  };
17664
17719
  }
17665
17720
  }
17666
- function renderStarLineCells(stars, width, y, now) {
17721
+ function placeMeteorsInCells(cells, meteors, row, xMin, xMax, xOffset, now) {
17722
+ for (const meteor of meteors) for (const trail of getMeteorTrail(meteor, now)) {
17723
+ if (trail.y !== row || trail.x < xMin || trail.x >= xMax) continue;
17724
+ const localX = trail.x - xOffset;
17725
+ cells[localX] = {
17726
+ char: trail.char,
17727
+ style: trail.state === "bright" ? "bold" : "dim",
17728
+ width: 1
17729
+ };
17730
+ }
17731
+ }
17732
+ function renderStarLineCells(stars, meteors, width, y, now) {
17667
17733
  const cells = emptyCells(width);
17668
17734
  placeStarsInCells(cells, stars, y, 0, width, 0, now);
17735
+ placeMeteorsInCells(cells, meteors, y, 0, width, 0, now);
17669
17736
  return cells;
17670
17737
  }
17671
- function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
17738
+ function renderSideStarsCells(stars, meteors, rowIndex, xOffset, sideWidth, now) {
17672
17739
  if (sideWidth <= 0) return [];
17673
17740
  const cells = emptyCells(sideWidth);
17674
17741
  placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
17742
+ placeMeteorsInCells(cells, meteors, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
17675
17743
  return cells;
17676
17744
  }
17677
17745
  function clampCellsToWidth(content, width) {
@@ -17788,7 +17856,7 @@ function buildContentCells(prompt, agentName, state, elapsed, now, availableHeig
17788
17856
  }
17789
17857
  return rows;
17790
17858
  }
17791
- function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
17859
+ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight, topMeteors = [], bottomMeteors = [], sideMeteors = []) {
17792
17860
  const elapsed = formatElapsed(now - state.startTime.getTime());
17793
17861
  const availableHeight = Math.max(0, terminalHeight - 2);
17794
17862
  const contentRows = buildContentCells(prompt, agentName, state, elapsed, now, availableHeight);
@@ -17797,20 +17865,24 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
17797
17865
  const remaining = Math.max(0, availableHeight - contentCount);
17798
17866
  const topHeight = Math.max(0, Math.ceil(remaining / 2));
17799
17867
  const bottomHeight = remaining - topHeight;
17868
+ const maxMeteorStartRow = Math.ceil(availableHeight * .75);
17869
+ const visibleTopMeteors = meteorsStartingBefore(topMeteors, 0, maxMeteorStartRow);
17870
+ const visibleSideMeteors = meteorsStartingBefore(sideMeteors, topHeight, maxMeteorStartRow);
17871
+ const visibleBottomMeteors = meteorsStartingBefore(bottomMeteors, topHeight + contentCount, maxMeteorStartRow);
17800
17872
  const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
17801
17873
  const frame = [];
17802
- for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, terminalWidth, y, now));
17874
+ for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, visibleTopMeteors, terminalWidth, y, now));
17803
17875
  for (let i = 0; i < contentRows.length; i++) {
17804
- const left = renderSideStarsCells(sideStars, i, 0, sideWidth, now);
17876
+ const left = renderSideStarsCells(sideStars, visibleSideMeteors, i, 0, sideWidth, now);
17805
17877
  const center = centerLineCells(contentRows[i], CONTENT_WIDTH);
17806
- const right = renderSideStarsCells(sideStars, i, terminalWidth - sideWidth, sideWidth, now);
17878
+ const right = renderSideStarsCells(sideStars, visibleSideMeteors, i, terminalWidth - sideWidth, sideWidth, now);
17807
17879
  frame.push([
17808
17880
  ...left,
17809
17881
  ...center,
17810
17882
  ...right
17811
17883
  ]);
17812
17884
  }
17813
- for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
17885
+ for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, visibleBottomMeteors, terminalWidth, y, now));
17814
17886
  frame.push(renderResumeHintCells(terminalWidth, state.interruptHint));
17815
17887
  frame.push(emptyCells(terminalWidth));
17816
17888
  return frame;
@@ -17826,8 +17898,11 @@ var Renderer = class {
17826
17898
  topStars = [];
17827
17899
  bottomStars = [];
17828
17900
  sideStars = [];
17901
+ topMeteors = [];
17902
+ bottomMeteors = [];
17829
17903
  cachedWidth = 0;
17830
17904
  cachedHeight = 0;
17905
+ meteorFrequency;
17831
17906
  prevCells = [];
17832
17907
  prevTitle = null;
17833
17908
  titleSaved = false;
@@ -17846,11 +17921,12 @@ var Renderer = class {
17846
17921
  handleStopped = () => {
17847
17922
  this.stop("stopped");
17848
17923
  };
17849
- constructor(orchestrator, prompt, agentName, onInterrupt) {
17924
+ constructor(orchestrator, prompt, agentName, onInterrupt, options = {}) {
17850
17925
  this.orchestrator = orchestrator;
17851
17926
  this.prompt = prompt;
17852
17927
  this.agentName = agentName;
17853
17928
  this.onInterrupt = onInterrupt;
17929
+ this.meteorFrequency = Math.max(0, Math.floor(options.meteorFrequency ?? DEFAULT_METEOR_FREQUENCY));
17854
17930
  this.state = orchestrator.getState();
17855
17931
  this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
17856
17932
  this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
@@ -17903,6 +17979,7 @@ var Renderer = class {
17903
17979
  const availableHeight = Math.max(0, h - 2);
17904
17980
  const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
17905
17981
  const topHeight = Math.max(0, Math.ceil(remaining / 2));
17982
+ const bottomHeight = Math.max(0, remaining - topHeight);
17906
17983
  const proximityRows = 8;
17907
17984
  const shrinkBig = (s, nearContentRow) => {
17908
17985
  if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
@@ -17918,6 +17995,8 @@ var Renderer = class {
17918
17995
  this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
17919
17996
  this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
17920
17997
  this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
17998
+ this.topMeteors = generateMeteorShower(w, topHeight, topHeight > 0 ? meteorCountForFrequency(this.meteorFrequency) : 0, this.seedTop + METEOR_SEED_OFFSET);
17999
+ this.bottomMeteors = generateMeteorShower(w, bottomHeight, bottomHeight > 0 ? meteorCountForFrequency(this.meteorFrequency) : 0, this.seedBottom + METEOR_SEED_OFFSET);
17921
18000
  return true;
17922
18001
  }
17923
18002
  return false;
@@ -17928,7 +18007,7 @@ var Renderer = class {
17928
18007
  const h = process$1.stdout.rows || 24;
17929
18008
  const resized = this.ensureStarFields(w, h);
17930
18009
  this.updateTerminalTitle(now);
17931
- const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
18010
+ const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h, this.topMeteors, this.bottomMeteors);
17932
18011
  if (this.isFirstFrame || resized) {
17933
18012
  process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
17934
18013
  this.isFirstFrame = false;
@@ -17959,6 +18038,7 @@ function slugifyPrompt(prompt) {
17959
18038
  //#region src/cli.ts
17960
18039
  const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
17961
18040
  const FORCE_EXIT_TIMEOUT_MS = 5e3;
18041
+ const MAX_METEOR_FREQUENCY = 5;
17962
18042
  const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
17963
18043
  const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
17964
18044
  const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
@@ -17977,6 +18057,11 @@ function parseNonNegativeInteger(value) {
17977
18057
  if (!Number.isSafeInteger(parsed)) throw new InvalidArgumentError("must be a safe integer");
17978
18058
  return parsed;
17979
18059
  }
18060
+ function parseMeteorFrequency(value) {
18061
+ const parsed = parseNonNegativeInteger(value);
18062
+ if (parsed > MAX_METEOR_FREQUENCY) throw new InvalidArgumentError(`must be between 0 and ${MAX_METEOR_FREQUENCY}`);
18063
+ return parsed;
18064
+ }
17980
18065
  function parseOnOffBoolean(value) {
17981
18066
  if (value === "on" || value === "true") return true;
17982
18067
  if (value === "off" || value === "false") return false;
@@ -18264,13 +18349,13 @@ function readReexecStdinPrompt(env) {
18264
18349
  }
18265
18350
  }
18266
18351
  const program = new Command();
18267
- program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", `Agent to use (${AGENT_NAMES.join(", ")}, or acp:<target-or-command>)`).option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End when the agent reports this condition, after any commit-failure repair; resumes reuse it, pass a new value to overwrite or \"\" to clear").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--current-branch", "Run on the current branch instead of creating a gnhf branch", false).option("--push", "Push the current branch after each successful iteration", false).option("--mock", "", false).action(async (promptArg, options) => {
18352
+ program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", `Agent to use (${AGENT_NAMES.join(", ")}, or acp:<target-or-command>)`).option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End when the agent reports this condition, after any commit-failure repair; resumes reuse it, pass a new value to overwrite or \"\" to clear").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--current-branch", "Run on the current branch instead of creating a gnhf branch", false).option("--push", "Push the current branch after each successful iteration", false).option("--meteor-frequency <n>", "Meteor frequency from 0 to 5 (0 disables, 3 is default)", parseMeteorFrequency, 3).option("--mock", "", false).action(async (promptArg, options) => {
18268
18353
  if (options.mock) {
18269
18354
  const mock = new MockOrchestrator();
18270
18355
  enterAltScreen();
18271
- const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude", () => {
18356
+ const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "codex", () => {
18272
18357
  mock.handleInterrupt();
18273
- });
18358
+ }, { meteorFrequency: options.meteorFrequency });
18274
18359
  renderer.start();
18275
18360
  mock.start();
18276
18361
  await renderer.waitUntilExit();
@@ -18487,7 +18572,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18487
18572
  requestForceShutdown("SIGTERM");
18488
18573
  };
18489
18574
  enterAltScreen();
18490
- const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt);
18575
+ const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt, { meteorFrequency: options.meteorFrequency });
18491
18576
  renderer.start();
18492
18577
  process$1.on("SIGINT", handleSigInt);
18493
18578
  process$1.on("SIGTERM", handleSigTerm);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {