gnhf 0.1.19 → 0.1.21
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 -45
- 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
|
@@ -6,7 +6,7 @@ import process$1 from "node:process";
|
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
import { Command, InvalidArgumentError } from "commander";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
|
-
import { execFileSync,
|
|
9
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
10
10
|
import { createServer } from "node:net";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
@@ -302,32 +302,24 @@ const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git reposi
|
|
|
302
302
|
function translateGitError(error) {
|
|
303
303
|
return error instanceof Error ? error : new Error(String(error));
|
|
304
304
|
}
|
|
305
|
-
function git(args, cwd) {
|
|
305
|
+
function git(args, cwd, options = {}) {
|
|
306
306
|
try {
|
|
307
|
-
return
|
|
307
|
+
return execFileSync("git", args, {
|
|
308
308
|
cwd,
|
|
309
309
|
encoding: "utf-8",
|
|
310
|
-
stdio: "pipe"
|
|
310
|
+
stdio: "pipe",
|
|
311
|
+
...options.env ? { env: options.env } : {}
|
|
311
312
|
}).trim();
|
|
312
313
|
} catch (error) {
|
|
313
314
|
throw translateGitError(error);
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
|
-
/** Wrap a value in single quotes, escaping embedded single quotes for POSIX shells. */
|
|
317
|
-
function shellEscape(value) {
|
|
318
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
319
|
-
}
|
|
320
317
|
function isGitRepository(cwd) {
|
|
321
318
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
env: {
|
|
327
|
-
...process.env,
|
|
328
|
-
LC_ALL: "C"
|
|
329
|
-
}
|
|
330
|
-
});
|
|
319
|
+
git(["rev-parse", "--git-dir"], cwd, { env: {
|
|
320
|
+
...process.env,
|
|
321
|
+
LC_ALL: "C"
|
|
322
|
+
} });
|
|
331
323
|
return true;
|
|
332
324
|
} catch {
|
|
333
325
|
return false;
|
|
@@ -339,23 +331,41 @@ function ensureGitRepository(cwd) {
|
|
|
339
331
|
function getCurrentBranch(cwd) {
|
|
340
332
|
ensureGitRepository(cwd);
|
|
341
333
|
try {
|
|
342
|
-
return git(
|
|
334
|
+
return git([
|
|
335
|
+
"symbolic-ref",
|
|
336
|
+
"--short",
|
|
337
|
+
"HEAD"
|
|
338
|
+
], cwd);
|
|
343
339
|
} catch {
|
|
344
|
-
return git(
|
|
340
|
+
return git([
|
|
341
|
+
"rev-parse",
|
|
342
|
+
"--abbrev-ref",
|
|
343
|
+
"HEAD"
|
|
344
|
+
], cwd);
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
function ensureCleanWorkingTree(cwd) {
|
|
348
|
-
if (git("status --porcelain", cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
|
|
348
|
+
if (git(["status", "--porcelain"], cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
|
|
349
349
|
}
|
|
350
350
|
function createBranch(branchName, cwd) {
|
|
351
|
-
git(
|
|
351
|
+
git([
|
|
352
|
+
"checkout",
|
|
353
|
+
"-b",
|
|
354
|
+
branchName
|
|
355
|
+
], cwd);
|
|
352
356
|
}
|
|
353
357
|
function getHeadCommit(cwd) {
|
|
354
|
-
return git("rev-parse HEAD", cwd);
|
|
358
|
+
return git(["rev-parse", "HEAD"], cwd);
|
|
355
359
|
}
|
|
356
360
|
function findLegacyRunBaseCommit(runId, cwd) {
|
|
357
361
|
try {
|
|
358
|
-
const marker = git(
|
|
362
|
+
const marker = git([
|
|
363
|
+
"log",
|
|
364
|
+
"--first-parent",
|
|
365
|
+
"--reverse",
|
|
366
|
+
"--format=%H%x09%s",
|
|
367
|
+
"HEAD"
|
|
368
|
+
], cwd).split("\n").map((line) => {
|
|
359
369
|
const [sha, ...subjectParts] = line.split(" ");
|
|
360
370
|
return {
|
|
361
371
|
sha,
|
|
@@ -363,33 +373,57 @@ function findLegacyRunBaseCommit(runId, cwd) {
|
|
|
363
373
|
};
|
|
364
374
|
}).find(({ subject }) => subject === `gnhf: initialize run ${runId}` || subject === `gnhf: overwrite run ${runId}`);
|
|
365
375
|
if (!marker?.sha) return null;
|
|
366
|
-
return git(
|
|
376
|
+
return git(["rev-parse", `${marker.sha}^`], cwd);
|
|
367
377
|
} catch {
|
|
368
378
|
return null;
|
|
369
379
|
}
|
|
370
380
|
}
|
|
371
381
|
function getBranchCommitCount(baseCommit, cwd) {
|
|
372
382
|
if (!baseCommit) return 0;
|
|
373
|
-
return Number.parseInt(git(
|
|
383
|
+
return Number.parseInt(git([
|
|
384
|
+
"rev-list",
|
|
385
|
+
"--count",
|
|
386
|
+
"--first-parent",
|
|
387
|
+
`${baseCommit}..HEAD`
|
|
388
|
+
], cwd), 10);
|
|
374
389
|
}
|
|
375
390
|
function commitAll(message, cwd) {
|
|
376
|
-
git("add -A", cwd);
|
|
391
|
+
git(["add", "-A"], cwd);
|
|
377
392
|
try {
|
|
378
|
-
git(
|
|
393
|
+
git([
|
|
394
|
+
"commit",
|
|
395
|
+
"-m",
|
|
396
|
+
message
|
|
397
|
+
], cwd);
|
|
379
398
|
} catch {}
|
|
380
399
|
}
|
|
381
400
|
function resetHard(cwd) {
|
|
382
|
-
git(
|
|
383
|
-
|
|
401
|
+
git([
|
|
402
|
+
"reset",
|
|
403
|
+
"--hard",
|
|
404
|
+
"HEAD"
|
|
405
|
+
], cwd);
|
|
406
|
+
git(["clean", "-fd"], cwd);
|
|
384
407
|
}
|
|
385
408
|
function getRepoRootDir(cwd) {
|
|
386
|
-
return git("rev-parse --show-toplevel", cwd);
|
|
409
|
+
return git(["rev-parse", "--show-toplevel"], cwd);
|
|
387
410
|
}
|
|
388
411
|
function createWorktree(baseCwd, worktreePath, branchName) {
|
|
389
|
-
git(
|
|
412
|
+
git([
|
|
413
|
+
"worktree",
|
|
414
|
+
"add",
|
|
415
|
+
"-b",
|
|
416
|
+
branchName,
|
|
417
|
+
worktreePath
|
|
418
|
+
], baseCwd);
|
|
390
419
|
}
|
|
391
420
|
function removeWorktree(baseCwd, worktreePath) {
|
|
392
|
-
git(
|
|
421
|
+
git([
|
|
422
|
+
"worktree",
|
|
423
|
+
"remove",
|
|
424
|
+
"--force",
|
|
425
|
+
worktreePath
|
|
426
|
+
], baseCwd);
|
|
393
427
|
}
|
|
394
428
|
//#endregion
|
|
395
429
|
//#region src/core/agents/types.ts
|
|
@@ -3496,6 +3530,24 @@ const DONE_HINT = "[ctrl+c to exit]";
|
|
|
3496
3530
|
function spacedLabel(text) {
|
|
3497
3531
|
return text.split("").join(" ");
|
|
3498
3532
|
}
|
|
3533
|
+
function formatTokenCount(tokens, direction) {
|
|
3534
|
+
return `${formatTokens(tokens)} ${direction}`;
|
|
3535
|
+
}
|
|
3536
|
+
function formatCommitCount(commitCount) {
|
|
3537
|
+
return `${commitCount} ${commitCount === 1 ? "commit" : "commits"}`;
|
|
3538
|
+
}
|
|
3539
|
+
function buildTerminalTitle(state, now) {
|
|
3540
|
+
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)}`;
|
|
3541
|
+
}
|
|
3542
|
+
function emitTerminalTitle(title) {
|
|
3543
|
+
return `\x1b]2;${title}\x07`;
|
|
3544
|
+
}
|
|
3545
|
+
function saveTerminalTitle() {
|
|
3546
|
+
return "\x1B[22;0t";
|
|
3547
|
+
}
|
|
3548
|
+
function restoreTerminalTitle() {
|
|
3549
|
+
return "\x1B[23;0t";
|
|
3550
|
+
}
|
|
3499
3551
|
function renderTitleCells(agentName) {
|
|
3500
3552
|
return [
|
|
3501
3553
|
[...textToCells(spacedLabel("gnhf"), "dim"), ...agentName ? [
|
|
@@ -3511,21 +3563,20 @@ function renderTitleCells(agentName) {
|
|
|
3511
3563
|
];
|
|
3512
3564
|
}
|
|
3513
3565
|
function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
|
|
3514
|
-
const commitLabel = commitCount === 1 ? "commit" : "commits";
|
|
3515
3566
|
return [
|
|
3516
3567
|
...textToCells(elapsed, "bold"),
|
|
3517
3568
|
...textToCells(" ", "normal"),
|
|
3518
3569
|
...textToCells("·", "dim"),
|
|
3519
3570
|
...textToCells(" ", "normal"),
|
|
3520
|
-
...textToCells(
|
|
3571
|
+
...textToCells(formatTokenCount(inputTokens, "in"), "normal"),
|
|
3521
3572
|
...textToCells(" ", "normal"),
|
|
3522
3573
|
...textToCells("·", "dim"),
|
|
3523
3574
|
...textToCells(" ", "normal"),
|
|
3524
|
-
...textToCells(
|
|
3575
|
+
...textToCells(formatTokenCount(outputTokens, "out"), "normal"),
|
|
3525
3576
|
...textToCells(" ", "normal"),
|
|
3526
3577
|
...textToCells("·", "dim"),
|
|
3527
3578
|
...textToCells(" ", "normal"),
|
|
3528
|
-
...textToCells(
|
|
3579
|
+
...textToCells(formatCommitCount(commitCount), "normal")
|
|
3529
3580
|
];
|
|
3530
3581
|
}
|
|
3531
3582
|
function renderAgentMessageCells(message, status) {
|
|
@@ -3741,10 +3792,22 @@ var Renderer = class {
|
|
|
3741
3792
|
cachedWidth = 0;
|
|
3742
3793
|
cachedHeight = 0;
|
|
3743
3794
|
prevCells = [];
|
|
3795
|
+
prevTitle = null;
|
|
3796
|
+
titleSaved = false;
|
|
3744
3797
|
isFirstFrame = true;
|
|
3745
3798
|
seedTop;
|
|
3746
3799
|
seedBottom;
|
|
3747
3800
|
seedSide;
|
|
3801
|
+
handleState = (newState) => {
|
|
3802
|
+
this.state = {
|
|
3803
|
+
...newState,
|
|
3804
|
+
iterations: [...newState.iterations]
|
|
3805
|
+
};
|
|
3806
|
+
this.updateTerminalTitle();
|
|
3807
|
+
};
|
|
3808
|
+
handleStopped = () => {
|
|
3809
|
+
this.stop("stopped");
|
|
3810
|
+
};
|
|
3748
3811
|
constructor(orchestrator, prompt, agentName) {
|
|
3749
3812
|
this.orchestrator = orchestrator;
|
|
3750
3813
|
this.prompt = prompt;
|
|
@@ -3758,15 +3821,8 @@ var Renderer = class {
|
|
|
3758
3821
|
});
|
|
3759
3822
|
}
|
|
3760
3823
|
start() {
|
|
3761
|
-
this.orchestrator.on("state",
|
|
3762
|
-
|
|
3763
|
-
...newState,
|
|
3764
|
-
iterations: [...newState.iterations]
|
|
3765
|
-
};
|
|
3766
|
-
});
|
|
3767
|
-
this.orchestrator.on("stopped", () => {
|
|
3768
|
-
this.stop("stopped");
|
|
3769
|
-
});
|
|
3824
|
+
this.orchestrator.on("state", this.handleState);
|
|
3825
|
+
this.orchestrator.on("stopped", this.handleStopped);
|
|
3770
3826
|
if (process$1.stdin.isTTY) {
|
|
3771
3827
|
process$1.stdin.setRawMode(true);
|
|
3772
3828
|
process$1.stdin.resume();
|
|
@@ -3785,11 +3841,18 @@ var Renderer = class {
|
|
|
3785
3841
|
clearInterval(this.interval);
|
|
3786
3842
|
this.interval = null;
|
|
3787
3843
|
}
|
|
3844
|
+
this.orchestrator.off("state", this.handleState);
|
|
3845
|
+
this.orchestrator.off("stopped", this.handleStopped);
|
|
3788
3846
|
if (process$1.stdin.isTTY) {
|
|
3789
3847
|
process$1.stdin.setRawMode(false);
|
|
3790
3848
|
process$1.stdin.pause();
|
|
3791
3849
|
process$1.stdin.removeAllListeners("data");
|
|
3792
3850
|
}
|
|
3851
|
+
if (this.titleSaved) {
|
|
3852
|
+
process$1.stdout.write(restoreTerminalTitle());
|
|
3853
|
+
this.titleSaved = false;
|
|
3854
|
+
this.prevTitle = null;
|
|
3855
|
+
}
|
|
3793
3856
|
this.exitResolve(reason);
|
|
3794
3857
|
}
|
|
3795
3858
|
waitUntilExit() {
|
|
@@ -3828,6 +3891,7 @@ var Renderer = class {
|
|
|
3828
3891
|
const w = process$1.stdout.columns || 80;
|
|
3829
3892
|
const h = process$1.stdout.rows || 24;
|
|
3830
3893
|
const resized = this.ensureStarFields(w, h);
|
|
3894
|
+
this.updateTerminalTitle(now);
|
|
3831
3895
|
const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
|
|
3832
3896
|
if (this.isFirstFrame || resized) {
|
|
3833
3897
|
process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
|
|
@@ -3838,6 +3902,17 @@ var Renderer = class {
|
|
|
3838
3902
|
}
|
|
3839
3903
|
this.prevCells = nextCells;
|
|
3840
3904
|
}
|
|
3905
|
+
updateTerminalTitle(now = Date.now()) {
|
|
3906
|
+
if (!process$1.stdout.isTTY) return;
|
|
3907
|
+
const nextTitle = buildTerminalTitle(this.state, now);
|
|
3908
|
+
if (!this.titleSaved) {
|
|
3909
|
+
process$1.stdout.write(saveTerminalTitle());
|
|
3910
|
+
this.titleSaved = true;
|
|
3911
|
+
}
|
|
3912
|
+
if (nextTitle === this.prevTitle) return;
|
|
3913
|
+
process$1.stdout.write(emitTerminalTitle(nextTitle));
|
|
3914
|
+
this.prevTitle = nextTitle;
|
|
3915
|
+
}
|
|
3841
3916
|
};
|
|
3842
3917
|
//#endregion
|
|
3843
3918
|
//#region src/utils/slugify.ts
|