gnhf 0.1.20 → 0.1.22

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 +9 -8
  2. package/dist/cli.mjs +120 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,6 +46,7 @@ You wake up to a branch full of clean work and a log of everything that happened
46
46
 
47
47
  - **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
48
48
  - **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries and exponential backoff
49
+ - **Live terminal title** — interactive runs keep your terminal title updated with live status, token totals, and commit count, then restore the previous title on exit
49
50
  - **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
50
51
 
51
52
  ## Quick Start
@@ -166,14 +167,14 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
166
167
 
167
168
  ### Flags
168
169
 
169
- | Flag | Description | Default |
170
- | ------------------------ | ------------------------------------------------------------------ | ---------------------- |
171
- | `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
172
- | `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
173
- | `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
174
- | `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
175
- | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
176
- | `--version` | Show version | |
170
+ | Flag | Description | Default |
171
+ | ------------------------ | --------------------------------------------------------------------- | ---------------------- |
172
+ | `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
173
+ | `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
174
+ | `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
175
+ | `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
176
+ | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
177
+ | `--version` | Show version | |
177
178
 
178
179
  ## Configuration
179
180
 
package/dist/cli.mjs CHANGED
@@ -1082,6 +1082,20 @@ function buildClaudeArgs(prompt, extraArgs) {
1082
1082
  ...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
1083
1083
  ];
1084
1084
  }
1085
+ function toTokenUsage(usage) {
1086
+ return {
1087
+ inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
1088
+ outputTokens: usage.output_tokens ?? 0,
1089
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
1090
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
1091
+ };
1092
+ }
1093
+ function isSameUsage(a, b) {
1094
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
1095
+ }
1096
+ function extendsUsage(next, previous) {
1097
+ return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage(next, previous);
1098
+ }
1085
1099
  var ClaudeAgent = class {
1086
1100
  name = "claude";
1087
1101
  bin;
@@ -1115,13 +1129,60 @@ var ClaudeAgent = class {
1115
1129
  cacheReadTokens: 0,
1116
1130
  cacheCreationTokens: 0
1117
1131
  };
1132
+ const usageByMessageId = /* @__PURE__ */ new Map();
1133
+ let anonymousAssistantCount = 0;
1134
+ let lastAnonymousAssistantId = null;
1135
+ let lastAnonymousAssistantUsage = null;
1136
+ let pendingAnonymousAssistantUsage = null;
1118
1137
  parseJSONLStream(child.stdout, logStream, (event) => {
1119
1138
  if (event.type === "assistant") {
1120
1139
  const msg = event.message;
1121
- cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
1122
- cumulative.outputTokens += msg.usage.output_tokens ?? 0;
1123
- cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
1124
- cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
1140
+ const nextUsage = toTokenUsage(msg.usage);
1141
+ let messageId = msg.id;
1142
+ let previousUsage;
1143
+ if (messageId) {
1144
+ previousUsage = usageByMessageId.get(messageId);
1145
+ lastAnonymousAssistantId = null;
1146
+ lastAnonymousAssistantUsage = null;
1147
+ pendingAnonymousAssistantUsage = null;
1148
+ } else if (pendingAnonymousAssistantUsage && extendsUsage(nextUsage, pendingAnonymousAssistantUsage)) {
1149
+ messageId = `assistant-${anonymousAssistantCount++}`;
1150
+ previousUsage = pendingAnonymousAssistantUsage;
1151
+ cumulative.inputTokens += pendingAnonymousAssistantUsage.inputTokens;
1152
+ cumulative.outputTokens += pendingAnonymousAssistantUsage.outputTokens;
1153
+ cumulative.cacheReadTokens += pendingAnonymousAssistantUsage.cacheReadTokens;
1154
+ cumulative.cacheCreationTokens += pendingAnonymousAssistantUsage.cacheCreationTokens;
1155
+ usageByMessageId.set(messageId, pendingAnonymousAssistantUsage);
1156
+ pendingAnonymousAssistantUsage = null;
1157
+ lastAnonymousAssistantId = messageId;
1158
+ lastAnonymousAssistantUsage = nextUsage;
1159
+ } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && extendsUsage(nextUsage, lastAnonymousAssistantUsage)) {
1160
+ messageId = lastAnonymousAssistantId;
1161
+ previousUsage = usageByMessageId.get(messageId);
1162
+ pendingAnonymousAssistantUsage = null;
1163
+ lastAnonymousAssistantUsage = nextUsage;
1164
+ } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage(nextUsage, lastAnonymousAssistantUsage)) {
1165
+ messageId = lastAnonymousAssistantId;
1166
+ previousUsage = usageByMessageId.get(messageId);
1167
+ pendingAnonymousAssistantUsage ??= nextUsage;
1168
+ } else {
1169
+ messageId = `assistant-${anonymousAssistantCount++}`;
1170
+ pendingAnonymousAssistantUsage = null;
1171
+ lastAnonymousAssistantId = messageId;
1172
+ lastAnonymousAssistantUsage = nextUsage;
1173
+ }
1174
+ if (previousUsage) {
1175
+ cumulative.inputTokens += nextUsage.inputTokens - previousUsage.inputTokens;
1176
+ cumulative.outputTokens += nextUsage.outputTokens - previousUsage.outputTokens;
1177
+ cumulative.cacheReadTokens += nextUsage.cacheReadTokens - previousUsage.cacheReadTokens;
1178
+ cumulative.cacheCreationTokens += nextUsage.cacheCreationTokens - previousUsage.cacheCreationTokens;
1179
+ } else {
1180
+ cumulative.inputTokens += nextUsage.inputTokens;
1181
+ cumulative.outputTokens += nextUsage.outputTokens;
1182
+ cumulative.cacheReadTokens += nextUsage.cacheReadTokens;
1183
+ cumulative.cacheCreationTokens += nextUsage.cacheCreationTokens;
1184
+ }
1185
+ usageByMessageId.set(messageId, nextUsage);
1125
1186
  onUsage?.({ ...cumulative });
1126
1187
  if (onMessage) {
1127
1188
  const content = msg.content;
@@ -1146,12 +1207,7 @@ var ClaudeAgent = class {
1146
1207
  return;
1147
1208
  }
1148
1209
  const output = resultEvent.structured_output;
1149
- const usage = {
1150
- inputTokens: (resultEvent.usage.input_tokens ?? 0) + (resultEvent.usage.cache_read_input_tokens ?? 0),
1151
- outputTokens: resultEvent.usage.output_tokens ?? 0,
1152
- cacheReadTokens: resultEvent.usage.cache_read_input_tokens ?? 0,
1153
- cacheCreationTokens: resultEvent.usage.cache_creation_input_tokens ?? 0
1154
- };
1210
+ const usage = toTokenUsage(resultEvent.usage);
1155
1211
  onUsage?.(usage);
1156
1212
  resolve({
1157
1213
  output,
@@ -3530,6 +3586,24 @@ const DONE_HINT = "[ctrl+c to exit]";
3530
3586
  function spacedLabel(text) {
3531
3587
  return text.split("").join(" ");
3532
3588
  }
3589
+ function formatTokenCount(tokens, direction) {
3590
+ return `${formatTokens(tokens)} ${direction}`;
3591
+ }
3592
+ function formatCommitCount(commitCount) {
3593
+ return `${commitCount} ${commitCount === 1 ? "commit" : "commits"}`;
3594
+ }
3595
+ function buildTerminalTitle(state, now) {
3596
+ return `gnhf ${state.status === "running" || state.status === "waiting" ? getMoonPhase("active", now, MOON_PHASE_PERIOD) : state.status} · ${formatTokenCount(state.totalInputTokens, "in")} · ${formatTokenCount(state.totalOutputTokens, "out")} · ${formatCommitCount(state.commitCount)}`;
3597
+ }
3598
+ function emitTerminalTitle(title) {
3599
+ return `\x1b]2;${title}\x07`;
3600
+ }
3601
+ function saveTerminalTitle() {
3602
+ return "\x1B[22;0t";
3603
+ }
3604
+ function restoreTerminalTitle() {
3605
+ return "\x1B[23;0t";
3606
+ }
3533
3607
  function renderTitleCells(agentName) {
3534
3608
  return [
3535
3609
  [...textToCells(spacedLabel("gnhf"), "dim"), ...agentName ? [
@@ -3545,21 +3619,20 @@ function renderTitleCells(agentName) {
3545
3619
  ];
3546
3620
  }
3547
3621
  function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
3548
- const commitLabel = commitCount === 1 ? "commit" : "commits";
3549
3622
  return [
3550
3623
  ...textToCells(elapsed, "bold"),
3551
3624
  ...textToCells(" ", "normal"),
3552
3625
  ...textToCells("·", "dim"),
3553
3626
  ...textToCells(" ", "normal"),
3554
- ...textToCells(`${formatTokens(inputTokens)} in`, "normal"),
3627
+ ...textToCells(formatTokenCount(inputTokens, "in"), "normal"),
3555
3628
  ...textToCells(" ", "normal"),
3556
3629
  ...textToCells("·", "dim"),
3557
3630
  ...textToCells(" ", "normal"),
3558
- ...textToCells(`${formatTokens(outputTokens)} out`, "normal"),
3631
+ ...textToCells(formatTokenCount(outputTokens, "out"), "normal"),
3559
3632
  ...textToCells(" ", "normal"),
3560
3633
  ...textToCells("·", "dim"),
3561
3634
  ...textToCells(" ", "normal"),
3562
- ...textToCells(`${commitCount} ${commitLabel}`, "normal")
3635
+ ...textToCells(formatCommitCount(commitCount), "normal")
3563
3636
  ];
3564
3637
  }
3565
3638
  function renderAgentMessageCells(message, status) {
@@ -3775,10 +3848,22 @@ var Renderer = class {
3775
3848
  cachedWidth = 0;
3776
3849
  cachedHeight = 0;
3777
3850
  prevCells = [];
3851
+ prevTitle = null;
3852
+ titleSaved = false;
3778
3853
  isFirstFrame = true;
3779
3854
  seedTop;
3780
3855
  seedBottom;
3781
3856
  seedSide;
3857
+ handleState = (newState) => {
3858
+ this.state = {
3859
+ ...newState,
3860
+ iterations: [...newState.iterations]
3861
+ };
3862
+ this.updateTerminalTitle();
3863
+ };
3864
+ handleStopped = () => {
3865
+ this.stop("stopped");
3866
+ };
3782
3867
  constructor(orchestrator, prompt, agentName) {
3783
3868
  this.orchestrator = orchestrator;
3784
3869
  this.prompt = prompt;
@@ -3792,15 +3877,8 @@ var Renderer = class {
3792
3877
  });
3793
3878
  }
3794
3879
  start() {
3795
- this.orchestrator.on("state", (newState) => {
3796
- this.state = {
3797
- ...newState,
3798
- iterations: [...newState.iterations]
3799
- };
3800
- });
3801
- this.orchestrator.on("stopped", () => {
3802
- this.stop("stopped");
3803
- });
3880
+ this.orchestrator.on("state", this.handleState);
3881
+ this.orchestrator.on("stopped", this.handleStopped);
3804
3882
  if (process$1.stdin.isTTY) {
3805
3883
  process$1.stdin.setRawMode(true);
3806
3884
  process$1.stdin.resume();
@@ -3819,11 +3897,18 @@ var Renderer = class {
3819
3897
  clearInterval(this.interval);
3820
3898
  this.interval = null;
3821
3899
  }
3900
+ this.orchestrator.off("state", this.handleState);
3901
+ this.orchestrator.off("stopped", this.handleStopped);
3822
3902
  if (process$1.stdin.isTTY) {
3823
3903
  process$1.stdin.setRawMode(false);
3824
3904
  process$1.stdin.pause();
3825
3905
  process$1.stdin.removeAllListeners("data");
3826
3906
  }
3907
+ if (this.titleSaved) {
3908
+ process$1.stdout.write(restoreTerminalTitle());
3909
+ this.titleSaved = false;
3910
+ this.prevTitle = null;
3911
+ }
3827
3912
  this.exitResolve(reason);
3828
3913
  }
3829
3914
  waitUntilExit() {
@@ -3862,6 +3947,7 @@ var Renderer = class {
3862
3947
  const w = process$1.stdout.columns || 80;
3863
3948
  const h = process$1.stdout.rows || 24;
3864
3949
  const resized = this.ensureStarFields(w, h);
3950
+ this.updateTerminalTitle(now);
3865
3951
  const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
3866
3952
  if (this.isFirstFrame || resized) {
3867
3953
  process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
@@ -3872,6 +3958,17 @@ var Renderer = class {
3872
3958
  }
3873
3959
  this.prevCells = nextCells;
3874
3960
  }
3961
+ updateTerminalTitle(now = Date.now()) {
3962
+ if (!process$1.stdout.isTTY) return;
3963
+ const nextTitle = buildTerminalTitle(this.state, now);
3964
+ if (!this.titleSaved) {
3965
+ process$1.stdout.write(saveTerminalTitle());
3966
+ this.titleSaved = true;
3967
+ }
3968
+ if (nextTitle === this.prevTitle) return;
3969
+ process$1.stdout.write(emitTerminalTitle(nextTitle));
3970
+ this.prevTitle = nextTitle;
3971
+ }
3875
3972
  };
3876
3973
  //#endregion
3877
3974
  //#region src/utils/slugify.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {