gnhf 0.1.3 → 0.1.5

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 +10 -7
  2. package/dist/cli.mjs +368 -289
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,3 @@
1
- # gnhf
2
-
3
1
  <p align="center">Before I go to bed, I tell my agents:</p>
4
2
  <h1 align="center">good night, have fun</h1>
5
3
 
@@ -41,7 +39,7 @@
41
39
  <img src="docs/splash.png" alt="gnhf — Good Night, Have Fun" width="800">
42
40
  </p>
43
41
 
44
- gnhf is a [ralph loop](https://ghuntley.com/ralph/), autoresearch-style orchestrator that keeps your agents running while you sleep — each iteration makes one small, committed, documented change towards an objective.
42
+ gnhf is a [ralph](https://ghuntley.com/ralph/), [autoresearch](https://github.com/karpathy/autoresearch)-style orchestrator that keeps your agents running while you sleep — each iteration makes one small, committed, documented change towards an objective.
45
43
  You wake up to a branch full of clean work and a log of everything that happened.
46
44
 
47
45
  - **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C
@@ -55,6 +53,8 @@ $ gnhf "reduce complexity of the codebase without changing functionality"
55
53
  # go to sleep
56
54
  ```
57
55
 
56
+ Run `gnhf` from inside a Git repository with a clean working tree. If you are starting from a plain directory, run `git init` first.
57
+
58
58
  ## Install
59
59
 
60
60
  **npm**
@@ -119,6 +119,7 @@ npm link
119
119
 
120
120
  - **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
121
121
  - **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
122
+ - **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
122
123
  - **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
123
124
 
124
125
  ## CLI Reference
@@ -132,10 +133,10 @@ npm link
132
133
 
133
134
  ### Flags
134
135
 
135
- | Flag | Description | Default |
136
- | ----------------- | ---------------------------------- | -------- |
137
- | `--agent <agent>` | Agent to use (`claude` or `codex`) | `claude` |
138
- | `--version` | Show version | |
136
+ | Flag | Description | Default |
137
+ | ----------------- | ---------------------------------- | ---------------------- |
138
+ | `--agent <agent>` | Agent to use (`claude` or `codex`) | config file (`claude`) |
139
+ | `--version` | Show version | |
139
140
 
140
141
  ## Configuration
141
142
 
@@ -149,6 +150,8 @@ agent: claude
149
150
  maxConsecutiveFailures: 3
150
151
  ```
151
152
 
153
+ If the file does not exist yet, `gnhf` creates it on first run using the resolved defaults.
154
+
152
155
  CLI flags override config file values.
153
156
 
154
157
  ## Development
package/dist/cli.mjs CHANGED
@@ -3,10 +3,10 @@ import { appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync,
3
3
  import process$1 from "node:process";
4
4
  import { createInterface } from "node:readline";
5
5
  import { Command } from "commander";
6
- import { join } from "node:path";
6
+ import { dirname, isAbsolute, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import yaml from "js-yaml";
9
- import { execSync, spawn } from "node:child_process";
9
+ import { execFileSync, execSync, spawn } from "node:child_process";
10
10
  import { EventEmitter } from "node:events";
11
11
  import { createHash } from "node:crypto";
12
12
  //#region src/core/config.ts
@@ -14,30 +14,83 @@ const DEFAULT_CONFIG = {
14
14
  agent: "claude",
15
15
  maxConsecutiveFailures: 3
16
16
  };
17
+ function isMissingConfigError(error) {
18
+ if (!(error instanceof Error)) return false;
19
+ return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
20
+ }
21
+ function serializeConfig(config) {
22
+ return `# Agent to use by default
23
+ agent: ${config.agent}
24
+
25
+ # Abort after this many consecutive failures
26
+ maxConsecutiveFailures: ${config.maxConsecutiveFailures}
27
+ `;
28
+ }
17
29
  function loadConfig(overrides) {
18
- const configPath = join(homedir(), ".gnhf", "config.yml");
30
+ const configDir = join(homedir(), ".gnhf");
31
+ const configPath = join(configDir, "config.yml");
19
32
  let fileConfig = {};
33
+ let shouldBootstrapConfig = false;
20
34
  try {
21
35
  const raw = readFileSync(configPath, "utf-8");
22
36
  fileConfig = yaml.load(raw) ?? {};
23
- } catch {}
24
- return {
37
+ } catch (error) {
38
+ if (isMissingConfigError(error)) shouldBootstrapConfig = true;
39
+ }
40
+ const resolvedConfig = {
25
41
  ...DEFAULT_CONFIG,
26
42
  ...fileConfig,
27
43
  ...overrides
28
44
  };
45
+ if (shouldBootstrapConfig) try {
46
+ mkdirSync(configDir, { recursive: true });
47
+ writeFileSync(configPath, serializeConfig(resolvedConfig), "utf-8");
48
+ } catch {}
49
+ return resolvedConfig;
29
50
  }
30
51
  //#endregion
31
52
  //#region src/core/git.ts
53
+ const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
54
+ function translateGitError(error) {
55
+ return error instanceof Error ? error : new Error(String(error));
56
+ }
32
57
  function git(args, cwd) {
33
- return execSync(`git ${args}`, {
34
- cwd,
35
- encoding: "utf-8",
36
- stdio: "pipe"
37
- }).trim();
58
+ try {
59
+ return execSync(`git ${args}`, {
60
+ cwd,
61
+ encoding: "utf-8",
62
+ stdio: "pipe"
63
+ }).trim();
64
+ } catch (error) {
65
+ throw translateGitError(error);
66
+ }
67
+ }
68
+ function isGitRepository(cwd) {
69
+ try {
70
+ execSync("git rev-parse --git-dir", {
71
+ cwd,
72
+ encoding: "utf-8",
73
+ stdio: "pipe",
74
+ env: {
75
+ ...process.env,
76
+ LC_ALL: "C"
77
+ }
78
+ });
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function ensureGitRepository(cwd) {
85
+ if (!isGitRepository(cwd)) throw new Error(NOT_GIT_REPOSITORY_MESSAGE);
38
86
  }
39
87
  function getCurrentBranch(cwd) {
40
- return git("rev-parse --abbrev-ref HEAD", cwd);
88
+ ensureGitRepository(cwd);
89
+ try {
90
+ return git("symbolic-ref --short HEAD", cwd);
91
+ } catch {
92
+ return git("rev-parse --abbrev-ref HEAD", cwd);
93
+ }
41
94
  }
42
95
  function ensureCleanWorkingTree(cwd) {
43
96
  if (git("status --porcelain", cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
@@ -45,6 +98,28 @@ function ensureCleanWorkingTree(cwd) {
45
98
  function createBranch(branchName, cwd) {
46
99
  git(`checkout -b ${branchName}`, cwd);
47
100
  }
101
+ function getHeadCommit(cwd) {
102
+ return git("rev-parse HEAD", cwd);
103
+ }
104
+ function findLegacyRunBaseCommit(runId, cwd) {
105
+ try {
106
+ const marker = git("log --first-parent --reverse --format=%H%x09%s HEAD", cwd).split("\n").map((line) => {
107
+ const [sha, ...subjectParts] = line.split(" ");
108
+ return {
109
+ sha,
110
+ subject: subjectParts.join(" ")
111
+ };
112
+ }).find(({ subject }) => subject === `gnhf: initialize run ${runId}` || subject === `gnhf: overwrite run ${runId}`);
113
+ if (!marker?.sha) return null;
114
+ return git(`rev-parse ${marker.sha}^`, cwd);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+ function getBranchCommitCount(baseCommit, cwd) {
120
+ if (!baseCommit) return 0;
121
+ return Number.parseInt(git(`rev-list --count --first-parent ${baseCommit}..HEAD`, cwd), 10);
122
+ }
48
123
  function commitAll(message, cwd) {
49
124
  git("add -A", cwd);
50
125
  try {
@@ -56,8 +131,8 @@ function resetHard(cwd) {
56
131
  git("clean -fd", cwd);
57
132
  }
58
133
  //#endregion
59
- //#region src/core/run.ts
60
- const OUTPUT_SCHEMA$1 = JSON.stringify({
134
+ //#region src/core/agents/types.ts
135
+ const AGENT_OUTPUT_SCHEMA = {
61
136
  type: "object",
62
137
  properties: {
63
138
  success: { type: "boolean" },
@@ -77,17 +152,29 @@ const OUTPUT_SCHEMA$1 = JSON.stringify({
77
152
  "key_changes_made",
78
153
  "key_learnings"
79
154
  ]
80
- }, null, 2);
81
- function ensureGitignore(cwd) {
82
- const gitignorePath = join(cwd, ".gitignore");
155
+ };
156
+ //#endregion
157
+ //#region src/core/run.ts
158
+ function ensureRunMetadataIgnored(cwd) {
159
+ const excludePath = execFileSync("git", [
160
+ "rev-parse",
161
+ "--git-path",
162
+ "info/exclude"
163
+ ], {
164
+ cwd,
165
+ encoding: "utf-8"
166
+ }).trim();
167
+ const resolved = isAbsolute(excludePath) ? excludePath : join(cwd, excludePath);
83
168
  const entry = ".gnhf/runs/";
84
- if (existsSync(gitignorePath)) {
85
- if (readFileSync(gitignorePath, "utf-8").split("\n").some((line) => line.trim() === entry)) return;
86
- appendFileSync(gitignorePath, `\n${entry}\n`, "utf-8");
87
- } else writeFileSync(gitignorePath, `${entry}\n`, "utf-8");
169
+ mkdirSync(dirname(resolved), { recursive: true });
170
+ if (existsSync(resolved)) {
171
+ const content = readFileSync(resolved, "utf-8");
172
+ if (content.split("\n").some((line) => line.trim() === entry)) return;
173
+ appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
174
+ } else writeFileSync(resolved, `${entry}\n`, "utf-8");
88
175
  }
89
- function setupRun(runId, prompt, cwd) {
90
- ensureGitignore(cwd);
176
+ function setupRun(runId, prompt, baseCommit, cwd) {
177
+ ensureRunMetadataIgnored(cwd);
91
178
  const runDir = join(cwd, ".gnhf", "runs", runId);
92
179
  mkdirSync(runDir, { recursive: true });
93
180
  const promptPath = join(runDir, "prompt.md");
@@ -95,26 +182,43 @@ function setupRun(runId, prompt, cwd) {
95
182
  const notesPath = join(runDir, "notes.md");
96
183
  writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
97
184
  const schemaPath = join(runDir, "output-schema.json");
98
- writeFileSync(schemaPath, OUTPUT_SCHEMA$1, "utf-8");
185
+ writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
186
+ const baseCommitPath = join(runDir, "base-commit");
187
+ const hasStoredBaseCommit = existsSync(baseCommitPath);
188
+ const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
189
+ if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
99
190
  return {
100
191
  runId,
101
192
  runDir,
102
193
  promptPath,
103
194
  notesPath,
104
- schemaPath
195
+ schemaPath,
196
+ baseCommit: resolvedBaseCommit,
197
+ baseCommitPath
105
198
  };
106
199
  }
107
200
  function resumeRun(runId, cwd) {
108
201
  const runDir = join(cwd, ".gnhf", "runs", runId);
109
202
  if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
203
+ const promptPath = join(runDir, "prompt.md");
204
+ const notesPath = join(runDir, "notes.md");
205
+ const schemaPath = join(runDir, "output-schema.json");
206
+ const baseCommitPath = join(runDir, "base-commit");
110
207
  return {
111
208
  runId,
112
209
  runDir,
113
- promptPath: join(runDir, "prompt.md"),
114
- notesPath: join(runDir, "notes.md"),
115
- schemaPath: join(runDir, "output-schema.json")
210
+ promptPath,
211
+ notesPath,
212
+ schemaPath,
213
+ baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
214
+ baseCommitPath
116
215
  };
117
216
  }
217
+ function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
218
+ const baseCommit = findLegacyRunBaseCommit(runId, cwd) ?? getHeadCommit(cwd);
219
+ writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
220
+ return baseCommit;
221
+ }
118
222
  function getLastIterationNumber(runInfo) {
119
223
  const files = readdirSync(runInfo.runDir);
120
224
  let max = 0;
@@ -127,37 +231,81 @@ function getLastIterationNumber(runInfo) {
127
231
  }
128
232
  return max;
129
233
  }
234
+ function formatListSection(title, items) {
235
+ if (items.length === 0) return "";
236
+ return `**${title}:**\n${items.map((item) => `- ${item}`).join("\n")}\n`;
237
+ }
130
238
  function appendNotes(notesPath, iteration, summary, changes, learnings) {
131
239
  appendFileSync(notesPath, [
132
240
  `\n### Iteration ${iteration}\n`,
133
241
  `**Summary:** ${summary}\n`,
134
- changes.length > 0 ? `**Changes:**\n${changes.map((c) => `- ${c}`).join("\n")}\n` : "",
135
- learnings.length > 0 ? `**Learnings:**\n${learnings.map((l) => `- ${l}`).join("\n")}\n` : ""
242
+ formatListSection("Changes", changes),
243
+ formatListSection("Learnings", learnings)
136
244
  ].join("\n"), "utf-8");
137
245
  }
138
246
  //#endregion
139
- //#region src/core/agents/claude.ts
140
- const OUTPUT_SCHEMA = JSON.stringify({
141
- type: "object",
142
- properties: {
143
- success: { type: "boolean" },
144
- summary: { type: "string" },
145
- key_changes_made: {
146
- type: "array",
147
- items: { type: "string" }
148
- },
149
- key_learnings: {
150
- type: "array",
151
- items: { type: "string" }
247
+ //#region src/core/agents/stream-utils.ts
248
+ /**
249
+ * Wire stderr collection, spawn-error handling, and the common close-handler
250
+ * prefix (logStream.end + non-zero exit code rejection) for a child process.
251
+ * Calls `onSuccess` only when the process exits with code 0.
252
+ */
253
+ function setupChildProcessHandlers(child, agentName, logStream, reject, onSuccess) {
254
+ let stderr = "";
255
+ child.stderr.on("data", (data) => {
256
+ stderr += data.toString();
257
+ });
258
+ child.on("error", (err) => {
259
+ reject(/* @__PURE__ */ new Error(`Failed to spawn ${agentName}: ${err.message}`));
260
+ });
261
+ child.on("close", (code) => {
262
+ logStream?.end();
263
+ if (code !== 0) {
264
+ reject(/* @__PURE__ */ new Error(`${agentName} exited with code ${code}: ${stderr}`));
265
+ return;
152
266
  }
153
- },
154
- required: [
155
- "success",
156
- "summary",
157
- "key_changes_made",
158
- "key_learnings"
159
- ]
160
- });
267
+ onSuccess();
268
+ });
269
+ }
270
+ /**
271
+ * Parse a JSONL stream, calling the callback for each parsed event.
272
+ * Handles buffering of incomplete lines and skips unparseable lines.
273
+ */
274
+ function parseJSONLStream(stream, logStream, callback) {
275
+ let buffer = "";
276
+ stream.on("data", (data) => {
277
+ logStream?.write(data);
278
+ buffer += data.toString();
279
+ const lines = buffer.split("\n");
280
+ buffer = lines.pop() ?? "";
281
+ for (const line of lines) {
282
+ if (!line.trim()) continue;
283
+ try {
284
+ callback(JSON.parse(line));
285
+ } catch {}
286
+ }
287
+ });
288
+ }
289
+ /**
290
+ * Wire an AbortSignal to kill a child process.
291
+ * Returns true if the signal was already aborted (caller should return early).
292
+ */
293
+ function setupAbortHandler(signal, child, reject) {
294
+ if (!signal) return false;
295
+ const onAbort = () => {
296
+ child.kill("SIGTERM");
297
+ reject(/* @__PURE__ */ new Error("Agent was aborted"));
298
+ };
299
+ if (signal.aborted) {
300
+ onAbort();
301
+ return true;
302
+ }
303
+ signal.addEventListener("abort", onAbort, { once: true });
304
+ child.on("close", () => signal.removeEventListener("abort", onAbort));
305
+ return false;
306
+ }
307
+ //#endregion
308
+ //#region src/core/agents/claude.ts
161
309
  var ClaudeAgent = class {
162
310
  name = "claude";
163
311
  run(prompt, cwd, options) {
@@ -171,7 +319,7 @@ var ClaudeAgent = class {
171
319
  "--output-format",
172
320
  "stream-json",
173
321
  "--json-schema",
174
- OUTPUT_SCHEMA,
322
+ JSON.stringify(AGENT_OUTPUT_SCHEMA),
175
323
  "--dangerously-skip-permissions"
176
324
  ], {
177
325
  cwd,
@@ -182,20 +330,7 @@ var ClaudeAgent = class {
182
330
  ],
183
331
  env: process.env
184
332
  });
185
- if (signal) {
186
- const onAbort = () => {
187
- child.kill("SIGTERM");
188
- reject(/* @__PURE__ */ new Error("Agent was aborted"));
189
- };
190
- if (signal.aborted) {
191
- onAbort();
192
- return;
193
- }
194
- signal.addEventListener("abort", onAbort, { once: true });
195
- child.on("close", () => signal.removeEventListener("abort", onAbort));
196
- }
197
- let stderr = "";
198
- let buffer = "";
333
+ if (setupAbortHandler(signal, child, reject)) return;
199
334
  let resultEvent = null;
200
335
  const cumulative = {
201
336
  inputTokens: 0,
@@ -203,45 +338,24 @@ var ClaudeAgent = class {
203
338
  cacheReadTokens: 0,
204
339
  cacheCreationTokens: 0
205
340
  };
206
- child.stdout.on("data", (data) => {
207
- logStream?.write(data);
208
- buffer += data.toString();
209
- const lines = buffer.split("\n");
210
- buffer = lines.pop() ?? "";
211
- for (const line of lines) {
212
- if (!line.trim()) continue;
213
- try {
214
- const event = JSON.parse(line);
215
- if (event.type === "assistant") {
216
- const msg = event.message;
217
- cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
218
- cumulative.outputTokens += msg.usage.output_tokens ?? 0;
219
- cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
220
- cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
221
- onUsage?.({ ...cumulative });
222
- if (onMessage) {
223
- const content = msg.content;
224
- if (Array.isArray(content)) {
225
- for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
226
- }
227
- }
341
+ parseJSONLStream(child.stdout, logStream, (event) => {
342
+ if (event.type === "assistant") {
343
+ const msg = event.message;
344
+ cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
345
+ cumulative.outputTokens += msg.usage.output_tokens ?? 0;
346
+ cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
347
+ cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
348
+ onUsage?.({ ...cumulative });
349
+ if (onMessage) {
350
+ const content = msg.content;
351
+ if (Array.isArray(content)) {
352
+ for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
228
353
  }
229
- if (event.type === "result") resultEvent = event;
230
- } catch {}
354
+ }
231
355
  }
356
+ if (event.type === "result") resultEvent = event;
232
357
  });
233
- child.stderr.on("data", (data) => {
234
- stderr += data.toString();
235
- });
236
- child.on("error", (err) => {
237
- reject(/* @__PURE__ */ new Error(`Failed to spawn claude: ${err.message}`));
238
- });
239
- child.on("close", (code) => {
240
- logStream?.end();
241
- if (code !== 0) {
242
- reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
243
- return;
244
- }
358
+ setupChildProcessHandlers(child, "claude", logStream, reject, () => {
245
359
  if (!resultEvent) {
246
360
  reject(/* @__PURE__ */ new Error("claude returned no result event"));
247
361
  return;
@@ -300,64 +414,28 @@ var CodexAgent = class {
300
414
  ],
301
415
  env: process.env
302
416
  });
303
- if (signal) {
304
- const onAbort = () => {
305
- child.kill("SIGTERM");
306
- reject(/* @__PURE__ */ new Error("Agent was aborted"));
307
- };
308
- if (signal.aborted) {
309
- onAbort();
310
- return;
311
- }
312
- signal.addEventListener("abort", onAbort, { once: true });
313
- child.on("close", () => signal.removeEventListener("abort", onAbort));
314
- }
315
- let stderr = "";
417
+ if (setupAbortHandler(signal, child, reject)) return;
316
418
  let lastAgentMessage = null;
317
- const usages = [];
318
- let buffer = "";
319
- child.stdout.on("data", (data) => {
320
- logStream?.write(data);
321
- buffer += data.toString();
322
- const lines = buffer.split("\n");
323
- buffer = lines.pop() ?? "";
324
- for (const line of lines) {
325
- if (!line.trim()) continue;
326
- try {
327
- const event = JSON.parse(line);
328
- if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
329
- lastAgentMessage = event.item.text;
330
- onMessage?.(lastAgentMessage);
331
- }
332
- if (event.type === "turn.completed" && "usage" in event) {
333
- usages.push(event.usage);
334
- if (onUsage) onUsage(usages.reduce((acc, u) => ({
335
- inputTokens: acc.inputTokens + (u.input_tokens ?? 0),
336
- outputTokens: acc.outputTokens + (u.output_tokens ?? 0),
337
- cacheReadTokens: acc.cacheReadTokens + (u.cached_input_tokens ?? 0),
338
- cacheCreationTokens: 0
339
- }), {
340
- inputTokens: 0,
341
- outputTokens: 0,
342
- cacheReadTokens: 0,
343
- cacheCreationTokens: 0
344
- }));
345
- }
346
- } catch {}
419
+ const cumulative = {
420
+ inputTokens: 0,
421
+ outputTokens: 0,
422
+ cacheReadTokens: 0,
423
+ cacheCreationTokens: 0
424
+ };
425
+ parseJSONLStream(child.stdout, logStream, (event) => {
426
+ if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
427
+ lastAgentMessage = event.item.text;
428
+ onMessage?.(lastAgentMessage);
347
429
  }
348
- });
349
- child.stderr.on("data", (data) => {
350
- stderr += data.toString();
351
- });
352
- child.on("error", (err) => {
353
- reject(/* @__PURE__ */ new Error(`Failed to spawn codex: ${err.message}`));
354
- });
355
- child.on("close", (code) => {
356
- logStream?.end();
357
- if (code !== 0) {
358
- reject(/* @__PURE__ */ new Error(`codex exited with code ${code}: ${stderr}`));
359
- return;
430
+ if (event.type === "turn.completed" && "usage" in event) {
431
+ const u = event.usage;
432
+ cumulative.inputTokens += u.input_tokens ?? 0;
433
+ cumulative.outputTokens += u.output_tokens ?? 0;
434
+ cumulative.cacheReadTokens += u.cached_input_tokens ?? 0;
435
+ onUsage?.({ ...cumulative });
360
436
  }
437
+ });
438
+ setupChildProcessHandlers(child, "codex", logStream, reject, () => {
361
439
  if (!lastAgentMessage) {
362
440
  reject(/* @__PURE__ */ new Error("codex returned no agent message"));
363
441
  return;
@@ -365,17 +443,7 @@ var CodexAgent = class {
365
443
  try {
366
444
  resolve({
367
445
  output: JSON.parse(lastAgentMessage),
368
- usage: usages.reduce((acc, u) => ({
369
- inputTokens: acc.inputTokens + (u.input_tokens ?? 0),
370
- outputTokens: acc.outputTokens + (u.output_tokens ?? 0),
371
- cacheReadTokens: acc.cacheReadTokens + (u.cached_input_tokens ?? 0),
372
- cacheCreationTokens: acc.cacheCreationTokens
373
- }), {
374
- inputTokens: 0,
375
- outputTokens: 0,
376
- cacheReadTokens: 0,
377
- cacheCreationTokens: 0
378
- })
446
+ usage: cumulative
379
447
  });
380
448
  } catch (err) {
381
449
  reject(/* @__PURE__ */ new Error(`Failed to parse codex output: ${err instanceof Error ? err.message : err}`));
@@ -432,6 +500,7 @@ var Orchestrator = class extends EventEmitter {
432
500
  currentIteration: 0,
433
501
  totalInputTokens: 0,
434
502
  totalOutputTokens: 0,
503
+ commitCount: 0,
435
504
  iterations: [],
436
505
  successCount: 0,
437
506
  failCount: 0,
@@ -448,6 +517,7 @@ var Orchestrator = class extends EventEmitter {
448
517
  this.prompt = prompt;
449
518
  this.cwd = cwd;
450
519
  this.state.currentIteration = startIteration;
520
+ this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
451
521
  }
452
522
  getState() {
453
523
  return { ...this.state };
@@ -474,69 +544,7 @@ var Orchestrator = class extends EventEmitter {
474
544
  runId: this.runInfo.runId,
475
545
  prompt: this.prompt
476
546
  });
477
- let record;
478
- const baseInputTokens = this.state.totalInputTokens;
479
- const baseOutputTokens = this.state.totalOutputTokens;
480
- this.activeAbortController = new AbortController();
481
- const onUsage = (usage) => {
482
- this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
483
- this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
484
- this.emit("state", this.getState());
485
- };
486
- const onMessage = (text) => {
487
- this.state.lastMessage = text;
488
- this.emit("state", this.getState());
489
- };
490
- const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
491
- try {
492
- const result = await this.agent.run(iterationPrompt, this.cwd, {
493
- onUsage,
494
- onMessage,
495
- signal: this.activeAbortController.signal,
496
- logPath
497
- });
498
- if (result.output.success) {
499
- appendNotes(this.runInfo.notesPath, this.state.currentIteration, result.output.summary, result.output.key_changes_made, result.output.key_learnings);
500
- commitAll(`gnhf #${this.state.currentIteration}: ${result.output.summary}`, this.cwd);
501
- this.state.successCount++;
502
- this.state.consecutiveFailures = 0;
503
- record = {
504
- number: this.state.currentIteration,
505
- success: true,
506
- summary: result.output.summary,
507
- keyChanges: result.output.key_changes_made,
508
- keyLearnings: result.output.key_learnings,
509
- timestamp: /* @__PURE__ */ new Date()
510
- };
511
- } else {
512
- appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[FAIL] ${result.output.summary}`, [], result.output.key_learnings);
513
- resetHard(this.cwd);
514
- this.state.failCount++;
515
- this.state.consecutiveFailures++;
516
- record = {
517
- number: this.state.currentIteration,
518
- success: false,
519
- summary: result.output.summary,
520
- keyChanges: [],
521
- keyLearnings: result.output.key_learnings,
522
- timestamp: /* @__PURE__ */ new Date()
523
- };
524
- }
525
- } catch (err) {
526
- const summary = err instanceof Error ? err.message : String(err);
527
- appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[ERROR] ${summary}`, [], []);
528
- resetHard(this.cwd);
529
- this.state.failCount++;
530
- this.state.consecutiveFailures++;
531
- record = {
532
- number: this.state.currentIteration,
533
- success: false,
534
- summary,
535
- keyChanges: [],
536
- keyLearnings: [],
537
- timestamp: /* @__PURE__ */ new Date()
538
- };
539
- }
547
+ const record = await this.runIteration(iterationPrompt);
540
548
  this.state.iterations.push(record);
541
549
  this.emit("iteration:end", record);
542
550
  this.emit("state", this.getState());
@@ -561,6 +569,63 @@ var Orchestrator = class extends EventEmitter {
561
569
  }
562
570
  }
563
571
  }
572
+ async runIteration(prompt) {
573
+ const baseInputTokens = this.state.totalInputTokens;
574
+ const baseOutputTokens = this.state.totalOutputTokens;
575
+ this.activeAbortController = new AbortController();
576
+ const onUsage = (usage) => {
577
+ this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
578
+ this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
579
+ this.emit("state", this.getState());
580
+ };
581
+ const onMessage = (text) => {
582
+ this.state.lastMessage = text;
583
+ this.emit("state", this.getState());
584
+ };
585
+ const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
586
+ try {
587
+ const result = await this.agent.run(prompt, this.cwd, {
588
+ onUsage,
589
+ onMessage,
590
+ signal: this.activeAbortController.signal,
591
+ logPath
592
+ });
593
+ if (result.output.success) return this.recordSuccess(result.output);
594
+ return this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings);
595
+ } catch (err) {
596
+ const summary = err instanceof Error ? err.message : String(err);
597
+ return this.recordFailure(`[ERROR] ${summary}`, summary, []);
598
+ }
599
+ }
600
+ recordSuccess(output) {
601
+ appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, output.key_changes_made, output.key_learnings);
602
+ commitAll(`gnhf #${this.state.currentIteration}: ${output.summary}`, this.cwd);
603
+ this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
604
+ this.state.successCount++;
605
+ this.state.consecutiveFailures = 0;
606
+ return {
607
+ number: this.state.currentIteration,
608
+ success: true,
609
+ summary: output.summary,
610
+ keyChanges: output.key_changes_made,
611
+ keyLearnings: output.key_learnings,
612
+ timestamp: /* @__PURE__ */ new Date()
613
+ };
614
+ }
615
+ recordFailure(notesSummary, recordSummary, learnings) {
616
+ appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], learnings);
617
+ resetHard(this.cwd);
618
+ this.state.failCount++;
619
+ this.state.consecutiveFailures++;
620
+ return {
621
+ number: this.state.currentIteration,
622
+ success: false,
623
+ summary: recordSummary,
624
+ keyChanges: [],
625
+ keyLearnings: learnings,
626
+ timestamp: /* @__PURE__ */ new Date()
627
+ };
628
+ }
564
629
  interruptibleSleep(ms) {
565
630
  return new Promise((resolve) => {
566
631
  this.activeAbortController = new AbortController();
@@ -635,6 +700,7 @@ var MockOrchestrator = class extends EventEmitter {
635
700
  currentIteration: 14,
636
701
  totalInputTokens: 873e5,
637
702
  totalOutputTokens: 86e4,
703
+ commitCount: 11,
638
704
  iterations: [...MOCK_ITERATIONS],
639
705
  successCount: 11,
640
706
  failCount: 2,
@@ -725,14 +791,10 @@ function generateStarField(width, height, density, seed) {
725
791
  function getStarState(star, now) {
726
792
  const t = (now % star.period / star.period + star.phase / (Math.PI * 2)) % 1;
727
793
  if (t > .05) return star.rest;
728
- if (star.rest === "bright") {
794
+ if (star.rest === "bright" || star.rest === "hidden") {
795
+ const opposite = star.rest === "bright" ? "hidden" : "bright";
729
796
  if (t > .0325) return "dim";
730
- if (t > .0175) return "hidden";
731
- return "dim";
732
- }
733
- if (star.rest === "hidden") {
734
- if (t > .0325) return "dim";
735
- if (t > .0175) return "bright";
797
+ if (t > .0175) return opposite;
736
798
  return "dim";
737
799
  }
738
800
  if (t > .025) return "bright";
@@ -906,6 +968,7 @@ const MOONS_PER_ROW = 30;
906
968
  const MOON_PHASE_PERIOD = 1600;
907
969
  const MAX_MSG_LINES = 3;
908
970
  const MAX_MSG_LINE_LEN = 64;
971
+ const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
909
972
  function renderTitleCells() {
910
973
  return [
911
974
  textToCells("g n h f", "dim"),
@@ -915,7 +978,8 @@ function renderTitleCells() {
915
978
  textToCells("┗━┛┗━┛┗━┛╺┻┛ ╹ ╹╹┗━┛╹ ╹ ╹ ╹ ╹╹ ╹┗┛ ┗━╸ ╹ ┗━┛╹ ╹", "bold")
916
979
  ];
917
980
  }
918
- function renderStatsCells(elapsed, inputTokens, outputTokens) {
981
+ function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
982
+ const commitLabel = commitCount === 1 ? "commit" : "commits";
919
983
  return [
920
984
  ...textToCells(elapsed, "bold"),
921
985
  ...textToCells(" ", "normal"),
@@ -925,7 +989,11 @@ function renderStatsCells(elapsed, inputTokens, outputTokens) {
925
989
  ...textToCells(" ", "normal"),
926
990
  ...textToCells("·", "dim"),
927
991
  ...textToCells(" ", "normal"),
928
- ...textToCells(`${formatTokens(outputTokens)} out`, "normal")
992
+ ...textToCells(`${formatTokens(outputTokens)} out`, "normal"),
993
+ ...textToCells(" ", "normal"),
994
+ ...textToCells("·", "dim"),
995
+ ...textToCells(" ", "normal"),
996
+ ...textToCells(`${commitCount} ${commitLabel}`, "normal")
929
997
  ];
930
998
  }
931
999
  function renderAgentMessageCells(message, status) {
@@ -958,42 +1026,31 @@ function starStyle(state) {
958
1026
  if (state === "dim") return "dim";
959
1027
  return "normal";
960
1028
  }
961
- function renderStarLineCells(stars, width, y, now) {
962
- const cells = emptyCells(width);
1029
+ function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
963
1030
  for (const star of stars) {
964
- if (star.y !== y) continue;
1031
+ if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
965
1032
  const state = getStarState(star, now);
966
- if (state === "hidden") cells[star.x] = {
1033
+ const localX = star.x - xOffset;
1034
+ cells[localX] = state === "hidden" ? {
967
1035
  char: " ",
968
1036
  style: "normal",
969
1037
  width: 1
970
- };
971
- else cells[star.x] = {
1038
+ } : {
972
1039
  char: star.char,
973
1040
  style: starStyle(state),
974
1041
  width: 1
975
1042
  };
976
1043
  }
1044
+ }
1045
+ function renderStarLineCells(stars, width, y, now) {
1046
+ const cells = emptyCells(width);
1047
+ placeStarsInCells(cells, stars, y, 0, width, 0, now);
977
1048
  return cells;
978
1049
  }
979
1050
  function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
980
1051
  if (sideWidth <= 0) return [];
981
1052
  const cells = emptyCells(sideWidth);
982
- for (const star of stars) {
983
- if (star.y !== rowIndex || star.x < xOffset || star.x >= xOffset + sideWidth) continue;
984
- const localX = star.x - xOffset;
985
- const state = getStarState(star, now);
986
- if (state === "hidden") cells[localX] = {
987
- char: " ",
988
- style: "normal",
989
- width: 1
990
- };
991
- else cells[localX] = {
992
- char: star.char,
993
- style: starStyle(state),
994
- width: 1
995
- };
996
- }
1053
+ placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
997
1054
  return cells;
998
1055
  }
999
1056
  function centerLineCells(content, width) {
@@ -1006,6 +1063,19 @@ function centerLineCells(content, width) {
1006
1063
  ...emptyCells(rightPad)
1007
1064
  ];
1008
1065
  }
1066
+ function renderResumeHintCells(width) {
1067
+ return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
1068
+ }
1069
+ function fitContentRows(contentRows, maxRows) {
1070
+ if (contentRows.length <= maxRows) return contentRows;
1071
+ const fitted = [...contentRows];
1072
+ while (fitted.length > maxRows) {
1073
+ const emptyRowIndex = fitted.findIndex((row) => row.length === 0);
1074
+ if (emptyRowIndex === -1) break;
1075
+ fitted.splice(emptyRowIndex, 1);
1076
+ }
1077
+ return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
1078
+ }
1009
1079
  function buildContentCells(prompt, state, elapsed, now) {
1010
1080
  const rows = [];
1011
1081
  const isRunning = state.status === "running" || state.status === "waiting";
@@ -1018,7 +1088,7 @@ function buildContentCells(prompt, state, elapsed, now) {
1018
1088
  rows.push(pl ? textToCells(pl, "dim") : []);
1019
1089
  }
1020
1090
  rows.push([], []);
1021
- rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens));
1091
+ rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount));
1022
1092
  rows.push([], []);
1023
1093
  rows.push(...renderAgentMessageCells(state.lastMessage, state.status));
1024
1094
  rows.push([], []);
@@ -1026,11 +1096,13 @@ function buildContentCells(prompt, state, elapsed, now) {
1026
1096
  return rows;
1027
1097
  }
1028
1098
  function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
1029
- const contentRows = buildContentCells(prompt, state, formatElapsed(now - state.startTime.getTime()), now);
1030
- while (contentRows.length < BASE_CONTENT_ROWS) contentRows.push([]);
1099
+ const elapsed = formatElapsed(now - state.startTime.getTime());
1100
+ const availableHeight = Math.max(0, terminalHeight - 2);
1101
+ const contentRows = fitContentRows(buildContentCells(prompt, state, elapsed, now), availableHeight);
1102
+ while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
1031
1103
  const contentCount = contentRows.length;
1032
- const remaining = Math.max(0, terminalHeight - contentCount);
1033
- const topHeight = Math.ceil(remaining / 2) - 1;
1104
+ const remaining = Math.max(0, availableHeight - contentCount);
1105
+ const topHeight = Math.max(0, Math.ceil(remaining / 2));
1034
1106
  const bottomHeight = remaining - topHeight;
1035
1107
  const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
1036
1108
  const frame = [];
@@ -1046,6 +1118,8 @@ function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, t
1046
1118
  ]);
1047
1119
  }
1048
1120
  for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
1121
+ frame.push(renderResumeHintCells(terminalWidth));
1122
+ frame.push(emptyCells(terminalWidth));
1049
1123
  return frame;
1050
1124
  }
1051
1125
  var Renderer = class {
@@ -1111,8 +1185,9 @@ var Renderer = class {
1111
1185
  this.cachedHeight = h;
1112
1186
  const contentStart = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2) - 8);
1113
1187
  const contentEnd = contentStart + CONTENT_WIDTH + 16;
1114
- const remaining = Math.max(0, h - BASE_CONTENT_ROWS);
1115
- const topHeight = Math.ceil(remaining / 2) - 1;
1188
+ const availableHeight = Math.max(0, h - 2);
1189
+ const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
1190
+ const topHeight = Math.max(0, Math.ceil(remaining / 2));
1116
1191
  const proximityRows = 8;
1117
1192
  const shrinkBig = (s, nearContentRow) => {
1118
1193
  if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
@@ -1127,7 +1202,7 @@ var Renderer = class {
1127
1202
  };
1128
1203
  this.topStars = generateStarField(w, h, STAR_DENSITY, 42).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
1129
1204
  this.bottomStars = generateStarField(w, h, STAR_DENSITY, 137).map((s) => shrinkBig(s, s.y < proximityRows));
1130
- this.sideStars = generateStarField(w, BASE_CONTENT_ROWS, STAR_DENSITY, 99);
1205
+ this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, 99);
1131
1206
  return true;
1132
1207
  }
1133
1208
  return false;
@@ -1156,6 +1231,18 @@ function slugifyPrompt(prompt) {
1156
1231
  //#endregion
1157
1232
  //#region src/cli.ts
1158
1233
  const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
1234
+ function humanizeErrorMessage(message) {
1235
+ if (message.includes("not a git repository")) return "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
1236
+ return message;
1237
+ }
1238
+ function initializeNewBranch(prompt, cwd) {
1239
+ ensureCleanWorkingTree(cwd);
1240
+ const baseCommit = getHeadCommit(cwd);
1241
+ const branchName = slugifyPrompt(prompt);
1242
+ createBranch(branchName, cwd);
1243
+ const runId = branchName.split("/")[1];
1244
+ return setupRun(runId, prompt, baseCommit, cwd);
1245
+ }
1159
1246
  function ask(question) {
1160
1247
  const rl = createInterface({
1161
1248
  input: process$1.stdin,
@@ -1169,7 +1256,7 @@ function ask(question) {
1169
1256
  });
1170
1257
  }
1171
1258
  const program = new Command();
1172
- 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 (claude or codex)", "claude").option("--mock", "", false).action(async (promptArg, options) => {
1259
+ 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 (claude or codex)").option("--mock", "", false).action(async (promptArg, options) => {
1173
1260
  if (options.mock) {
1174
1261
  const mock = new MockOrchestrator();
1175
1262
  enterAltScreen();
@@ -1183,11 +1270,15 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
1183
1270
  let prompt = promptArg;
1184
1271
  if (!prompt && !process$1.stdin.isTTY) prompt = readFileSync("/dev/stdin", "utf-8").trim();
1185
1272
  const agentName = options.agent;
1186
- if (agentName !== "claude" && agentName !== "codex") {
1273
+ if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex") {
1187
1274
  console.error(`Unknown agent: ${options.agent}. Use "claude" or "codex".`);
1188
1275
  process$1.exit(1);
1189
1276
  }
1190
- const config = loadConfig({ agent: agentName });
1277
+ const config = loadConfig(agentName ? { agent: agentName } : void 0);
1278
+ if (config.agent !== "claude" && config.agent !== "codex") {
1279
+ console.error(`Unknown agent: ${config.agent}. Use "claude" or "codex".`);
1280
+ process$1.exit(1);
1281
+ }
1191
1282
  const cwd = process$1.cwd();
1192
1283
  const currentBranch = getCurrentBranch(cwd);
1193
1284
  const onGnhfBranch = currentBranch.startsWith("gnhf/");
@@ -1205,30 +1296,18 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
1205
1296
  const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Overwrite current run with new prompt\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `);
1206
1297
  if (answer === "o") {
1207
1298
  ensureCleanWorkingTree(cwd);
1208
- runInfo = setupRun(existingRunId, prompt, cwd);
1209
- commitAll(`gnhf: overwrite run ${existingRunId}`, cwd);
1210
- } else if (answer === "n") {
1211
- ensureCleanWorkingTree(cwd);
1212
- const branchName = slugifyPrompt(prompt);
1213
- createBranch(branchName, cwd);
1214
- const runId = branchName.split("/")[1];
1215
- runInfo = setupRun(runId, prompt, cwd);
1216
- commitAll(`gnhf: initialize run ${runId}`, cwd);
1217
- } else process$1.exit(0);
1299
+ runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
1300
+ } else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd);
1301
+ else process$1.exit(0);
1218
1302
  }
1219
1303
  } else {
1220
1304
  if (!prompt) {
1221
1305
  program.help();
1222
1306
  return;
1223
1307
  }
1224
- ensureCleanWorkingTree(cwd);
1225
- const branchName = slugifyPrompt(prompt);
1226
- createBranch(branchName, cwd);
1227
- const runId = branchName.split("/")[1];
1228
- runInfo = setupRun(runId, prompt, cwd);
1229
- commitAll(`gnhf: initialize run ${runId}`, cwd);
1308
+ runInfo = initializeNewBranch(prompt, cwd);
1230
1309
  }
1231
- const orchestrator = new Orchestrator(config, createAgent(agentName, runInfo), runInfo, prompt, cwd, startIteration);
1310
+ const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration);
1232
1311
  enterAltScreen();
1233
1312
  const renderer = new Renderer(orchestrator, prompt);
1234
1313
  renderer.start();
@@ -1249,7 +1328,7 @@ function exitAltScreen() {
1249
1328
  process$1.stdout.write("\x1B[?1049l");
1250
1329
  }
1251
1330
  function die(message) {
1252
- console.error(`\n gnhf: ${message}\n`);
1331
+ console.error(`\n gnhf: ${humanizeErrorMessage(message)}\n`);
1253
1332
  process$1.exit(1);
1254
1333
  }
1255
1334
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {