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.
- package/README.md +9 -8
- package/dist/cli.mjs +120 -23
- 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
|
|
170
|
-
| ------------------------ |
|
|
171
|
-
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`)
|
|
172
|
-
| `--max-iterations <n>` | Abort after `n` total iterations
|
|
173
|
-
| `--max-tokens <n>` | Abort after `n` total input+output tokens
|
|
174
|
-
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`)
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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(
|
|
3627
|
+
...textToCells(formatTokenCount(inputTokens, "in"), "normal"),
|
|
3555
3628
|
...textToCells(" ", "normal"),
|
|
3556
3629
|
...textToCells("·", "dim"),
|
|
3557
3630
|
...textToCells(" ", "normal"),
|
|
3558
|
-
...textToCells(
|
|
3631
|
+
...textToCells(formatTokenCount(outputTokens, "out"), "normal"),
|
|
3559
3632
|
...textToCells(" ", "normal"),
|
|
3560
3633
|
...textToCells("·", "dim"),
|
|
3561
3634
|
...textToCells(" ", "normal"),
|
|
3562
|
-
...textToCells(
|
|
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",
|
|
3796
|
-
|
|
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
|