gnhf 0.1.32 → 0.1.34

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 +28 -24
  2. package/dist/cli.mjs +395 -49
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -47,6 +47,7 @@ You wake up to a branch full of clean work and a log of everything that happened
47
47
  - **Dead simple** — one command starts an autonomous loop that runs until you request stop or a configured runtime cap is reached
48
48
  - **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries; retryable hard agent errors back off exponentially while agent-reported failures continue immediately
49
49
  - **Live terminal title** — interactive runs keep your terminal title updated with live status, token totals, and commit count, then clear or restore it on exit depending on terminal support; token totals prefixed with `~` are estimates
50
+ - **Exit summary**: every run ends with a permanent summary covering elapsed time, branch, iterations, tokens, branch diff stats, local notes/log paths, and review commands
50
51
  - **Agent-agnostic**: works with Claude Code, Codex, Rovo Dev, OpenCode, GitHub Copilot CLI, Pi, or ACP targets out of the box
51
52
 
52
53
  ## Quick Start
@@ -142,6 +143,7 @@ npm link
142
143
  - **Runtime caps** - `--max-iterations` stops before the next iteration begins, `--max-tokens` can abort mid-iteration once reported usage reaches the cap, and `--stop-when` ends the loop after an iteration whose agent output reports the natural-language condition is met; resumed runs reuse the saved stop condition unless you pass a new value, or `--stop-when ""` to clear it; uncommitted work is rolled back in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
143
144
  - **Iteration finalization** - agents are expected to finish validation, stop any background processes they started, and only then emit the final JSON result for the iteration
144
145
  - **Graceful interrupts** - in the interactive TUI, the first Ctrl+C requests a graceful stop and lets the current iteration finish (or ends backoff early), the second Ctrl+C force-stops immediately, and `SIGTERM` also force-stops immediately
146
+ - **Exit summary** - after shutdown cleanup, gnhf prints a permanent stdout summary with the final branch, elapsed time, iteration and token totals, branch diff stats, notes/debug-log paths, and review commands
145
147
  - **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
146
148
  - **Local run metadata** — gnhf stores prompt, notes, stop conditions, and commit-message convention metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
147
149
  - **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off; if you provide a different prompt, gnhf asks whether to update the saved prompt and continue with the existing history, start a new branch, or quit. New runs whose generated branch already exists use a numeric suffix such as `gnhf/<slug>-1`.
@@ -175,22 +177,22 @@ If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf as
175
177
 
176
178
  ### Flags
177
179
 
178
- | Flag | Description | Default |
179
- | ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------- |
180
- | `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, `opencode`, `copilot`, `pi`, or `acp:<target>`) | config file (`claude`) |
181
- | `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
182
- | `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
183
- | `--stop-when <cond>` | End the loop when the agent reports this condition; persists across resume | unlimited |
184
- | `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
185
- | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
186
- | `--version` | Show version | |
180
+ | Flag | Description | Default |
181
+ | ------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------------- |
182
+ | `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, `opencode`, `copilot`, `pi`, or `acp:<target-or-command>`) | config file (`claude`) |
183
+ | `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
184
+ | `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
185
+ | `--stop-when <cond>` | End the loop when the agent reports this condition; persists across resume | unlimited |
186
+ | `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
187
+ | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
188
+ | `--version` | Show version | |
187
189
 
188
190
  ## Configuration
189
191
 
190
192
  Config lives at `~/.gnhf/config.yml`:
191
193
 
192
194
  ```yaml
193
- # Agent to use by default (claude, codex, rovodev, opencode, copilot, pi, or acp:<target>)
195
+ # Agent to use by default (claude, codex, rovodev, opencode, copilot, pi, or acp:<target-or-command>)
194
196
  agent: claude
195
197
 
196
198
  # Custom paths to native agent binaries (optional)
@@ -225,7 +227,7 @@ agent: claude
225
227
  # staging: "node /opt/staging/agent.mjs"
226
228
 
227
229
  # Commit message convention (optional)
228
- # Defaults to: gnhf #<iteration>: <summary>
230
+ # Defaults to: gnhf <iteration>: <summary>
229
231
  # Use the conventional preset for semantic-release compatible headers:
230
232
  # commitMessage:
231
233
  # preset: conventional
@@ -245,6 +247,7 @@ The iteration and token caps are runtime-only flags and are not persisted in `co
245
247
  `agentArgsOverride.<name>` lets you pass through extra CLI flags for native agents (`claude`, `codex`, `rovodev`, `opencode`, `copilot`, or `pi`).
246
248
  ACP targets do not support path or arg overrides in this version.
247
249
  Use `acpRegistryOverrides` to map `acp:<target>` names to custom spawn commands for local, forked, or beta ACP agents.
250
+ You can also pass a raw custom ACP server command directly as a quoted `acp:` spec, for example `gnhf --agent 'acp:./bin/dev-acp --profile ci' "fix the tests"`.
248
251
 
249
252
  - Use it for agent-specific options like models, profiles, or reasoning settings without adding a dedicated `gnhf` config field for each one.
250
253
  - For `codex`, `claude`, and `copilot`, `gnhf` adds its usual non-interactive permission default only when you do not provide your own permission or execution-mode flag. If you set one explicitly, `gnhf` treats that as user-managed and does not add its default on top.
@@ -252,7 +255,7 @@ Use `acpRegistryOverrides` to map `acp:<target>` names to custom spawn commands
252
255
 
253
256
  `commitMessage` controls the subject line that gnhf uses for each successful iteration commit.
254
257
 
255
- - Omit it to keep the default `gnhf #<iteration>: <summary>` format.
258
+ - Omit it to keep the default `gnhf <iteration>: <summary>` format.
256
259
  - Set `preset: conventional` to ask the agent for `type` and optional `scope`, then commit as `type(scope): summary` for semantic-release style workflows. Valid types are `build`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `test`, and `chore`; invalid or missing types fall back to `chore`, and empty scopes are omitted.
257
260
  - The resolved commit-message convention is saved per run, so resuming a `gnhf/` branch keeps the original subject format even if `config.yml` changes later.
258
261
 
@@ -273,7 +276,8 @@ When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS:
273
276
 
274
277
  ## Debug Logs
275
278
 
276
- Every run writes a JSONL debug log to `.gnhf/runs/<runId>/gnhf.log` alongside `notes.md`. Lifecycle events for the orchestrator, agent, and HTTP requests are captured with elapsed timings and (for failures) the full `error.cause` chain which is what you need to tell a bare `TypeError: fetch failed` apart from an undici `UND_ERR_HEADERS_TIMEOUT`. The agent's own streaming output still goes to the per-iteration `iteration-<n>.jsonl` file next to it.
279
+ Every run writes a JSONL debug log to `.gnhf/runs/<runId>/gnhf.log` alongside `notes.md`. Lifecycle events for the orchestrator, agent, and HTTP requests are captured with elapsed timings and (for failures) the full `error.cause` chain, which is what you need to tell a bare `TypeError: fetch failed` apart from an undici `UND_ERR_HEADERS_TIMEOUT`. The agent's own streaming output still goes to the per-iteration `iteration-<n>.jsonl` file next to it.
280
+ Raw ACP command specs are redacted as `acp:custom`/`custom` in debug logs and related errors, so local paths or secrets in custom commands are not written to `gnhf.log`.
277
281
 
278
282
  Including a snippet of `gnhf.log` is the single most useful thing you can attach when filing an issue.
279
283
 
@@ -285,17 +289,17 @@ Set `GNHF_TELEMETRY=0` to turn it off.
285
289
 
286
290
  ## Agents
287
291
 
288
- `gnhf` supports six native agents plus ACP targets. ACP support is powered by [`acpx`](https://github.com/openclaw/acpx), which is bundled with `gnhf` and provides the runtime and agent registry for `acp:<target>` specs.
289
-
290
- | Agent | Flag | Requirements | Notes |
291
- | ------------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
292
- | Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode. After Claude emits a successful structured result, `gnhf` treats that result as final and shuts down any lingering Claude process tree after a short grace period. |
293
- | Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
294
- | GitHub Copilot CLI | `--agent copilot` | Install GitHub Copilot CLI and sign in first. | `gnhf` invokes `copilot` directly in non-interactive JSONL mode. Copilot currently exposes assistant output tokens, but not full input/cache token totals; see https://github.com/github/copilot-cli/issues/1152. |
295
- | Pi | `--agent pi` | Install the `pi` CLI and configure a usable provider/model first. | `gnhf` invokes `pi` directly in JSON mode, appends the final output schema to the prompt, and disables Pi session persistence with `--no-session`. |
296
- | Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. |
297
- | OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts. |
298
- | ACP target | `--agent acp:<target>` | Install and authenticate the target supported by the bundled [`acpx`](https://github.com/openclaw/acpx) registry, such as `acp:gemini`. | `gnhf` runs the target through ACP with a persistent per-run session under `.gnhf/runs/<runId>/acp-sessions`; token usage and `--max-tokens` use ACP `used` deltas when available, with prompt-length plus tool-call estimates as a fallback, and `agentPathOverride` and `agentArgsOverride` do not apply. |
292
+ `gnhf` supports six native agents plus ACP targets. ACP support is powered by [`acpx`](https://github.com/openclaw/acpx), which is bundled with `gnhf` and provides the runtime and agent registry for `acp:<target-or-command>` specs.
293
+
294
+ | Agent | Flag | Requirements | Notes |
295
+ | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
296
+ | Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode. After Claude emits a successful structured result, `gnhf` treats that result as final and shuts down any lingering Claude process tree after a short grace period. |
297
+ | Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
298
+ | GitHub Copilot CLI | `--agent copilot` | Install GitHub Copilot CLI and sign in first. | `gnhf` invokes `copilot` directly in non-interactive JSONL mode. Copilot currently exposes assistant output tokens, but not full input/cache token totals; see https://github.com/github/copilot-cli/issues/1152. |
299
+ | Pi | `--agent pi` | Install the `pi` CLI and configure a usable provider/model first. | `gnhf` invokes `pi` directly in JSON mode, appends the final output schema to the prompt, and disables Pi session persistence with `--no-session`. |
300
+ | Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. |
301
+ | OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts. |
302
+ | ACP target | `--agent acp:<target-or-command>` | Install and authenticate the target supported by the bundled [`acpx`](https://github.com/openclaw/acpx) registry, such as `acp:gemini`, or pass a quoted custom ACP server command. | `gnhf` runs the target through ACP with a persistent per-run session under `.gnhf/runs/<runId>/acp-sessions`; token usage and `--max-tokens` use ACP `used` deltas when available, with prompt-length plus tool-call estimates as a fallback, and `agentPathOverride` and `agentArgsOverride` do not apply. |
299
303
 
300
304
  ## Development
301
305
 
package/dist/cli.mjs CHANGED
@@ -37,12 +37,21 @@ const AGENT_NAMES = [
37
37
  "copilot",
38
38
  "pi"
39
39
  ];
40
- const ACP_SPEC_PATTERN = /^acp:[A-Za-z0-9][A-Za-z0-9._:-]*$/;
41
40
  function isAgentName$1(name) {
42
- return AGENT_NAMES.includes(name);
41
+ return typeof name === "string" && AGENT_NAMES.includes(name);
42
+ }
43
+ function hasDisallowedAcpTargetChar(target) {
44
+ for (let i = 0; i < target.length; i += 1) {
45
+ const code = target.charCodeAt(i);
46
+ if (code < 32 || code === 127) return true;
47
+ }
48
+ return false;
43
49
  }
44
50
  function isAcpSpec(spec) {
45
- return ACP_SPEC_PATTERN.test(spec);
51
+ if (typeof spec !== "string") return false;
52
+ if (!spec.startsWith("acp:")) return false;
53
+ const target = spec.slice(4);
54
+ return target.length > 0 && target.trim() === target && !hasDisallowedAcpTargetChar(target);
46
55
  }
47
56
  function isAgentSpec(spec) {
48
57
  return isAgentName$1(spec) || isAcpSpec(spec);
@@ -50,6 +59,16 @@ function isAgentSpec(spec) {
50
59
  function getAcpTarget(spec) {
51
60
  return spec.slice(4);
52
61
  }
62
+ function isNamedAcpTarget(target) {
63
+ return ACP_TARGET_NAME_PATTERN.test(target);
64
+ }
65
+ function redactAcpTargetForLogs(target) {
66
+ return isNamedAcpTarget(target) ? target : "custom";
67
+ }
68
+ function redactAgentSpecForLogs(spec) {
69
+ if (!spec.startsWith("acp:")) return spec;
70
+ return `acp:${redactAcpTargetForLogs(spec.slice(4))}`;
71
+ }
53
72
  const DEFAULT_CONFIG = {
54
73
  agent: "claude",
55
74
  agentPathOverride: {},
@@ -199,12 +218,19 @@ function serializeAgentArgsOverride(agentArgsOverride) {
199
218
  sortKeys: false
200
219
  }).trimEnd();
201
220
  }
221
+ function serializeAgent(agent) {
222
+ return yaml.dump({ agent }, {
223
+ lineWidth: -1,
224
+ noRefs: true,
225
+ sortKeys: false
226
+ }).trimEnd();
227
+ }
202
228
  function serializeConfig(config) {
203
229
  const agentPathOverrideSection = serializeAgentPathOverride(config.agentPathOverride);
204
230
  const agentArgsOverrideSection = serializeAgentArgsOverride(config.agentArgsOverride);
205
231
  const lines = [
206
- "# Agent to use by default: native agent name or acp:<target>",
207
- `agent: ${config.agent}`,
232
+ "# Agent to use by default: native agent name or acp:<target-or-command>",
233
+ serializeAgent(config.agent),
208
234
  "",
209
235
  "# Custom paths to native agent binaries (optional)",
210
236
  "# Paths may be absolute, bare executable names on PATH,",
@@ -237,14 +263,14 @@ function serializeConfig(config) {
237
263
  "# - high",
238
264
  "",
239
265
  "# Custom ACP target commands (optional)",
240
- "# Maps acp:<target> names to spawn commands. Useful for pinning a",
266
+ "# Maps acp:<target> names to spawn commands. Useful for naming a",
241
267
  "# local or beta build of an ACP agent.",
242
268
  "# acpRegistryOverrides:",
243
269
  "# my-fork: \"/usr/local/bin/my-claude-code-fork --acp\"",
244
270
  "# staging: \"node /opt/staging/agent.mjs\"",
245
271
  "",
246
272
  "# Commit message convention (optional)",
247
- "# Defaults to: gnhf #<iteration>: <summary>",
273
+ "# Defaults to: gnhf <iteration>: <summary>",
248
274
  "# Use Conventional Commits semantic-release headers:",
249
275
  "# commitMessage:",
250
276
  "# preset: conventional"
@@ -479,6 +505,62 @@ function getBranchCommitCount(baseCommit, cwd) {
479
505
  `${baseCommit}..HEAD`
480
506
  ], cwd), 10);
481
507
  }
508
+ function emptyBranchDiffStats$1() {
509
+ return {
510
+ commits: 0,
511
+ filesChanged: 0,
512
+ filesAdded: 0,
513
+ filesUpdated: 0,
514
+ filesDeleted: 0,
515
+ filesRenamed: 0,
516
+ binaryFiles: 0,
517
+ linesAdded: 0,
518
+ linesDeleted: 0
519
+ };
520
+ }
521
+ function getBranchDiffStats(baseCommit, cwd) {
522
+ if (!baseCommit) return emptyBranchDiffStats$1();
523
+ const range = `${baseCommit}..HEAD`;
524
+ const stats = emptyBranchDiffStats$1();
525
+ stats.commits = Number.parseInt(git([
526
+ "rev-list",
527
+ "--count",
528
+ "--first-parent",
529
+ range
530
+ ], cwd), 10);
531
+ const nameStatus = git([
532
+ "diff",
533
+ "--name-status",
534
+ range
535
+ ], cwd);
536
+ for (const line of nameStatus.split("\n")) {
537
+ if (!line) continue;
538
+ const [status] = line.split(" ");
539
+ stats.filesChanged++;
540
+ if (status === "A") stats.filesAdded++;
541
+ else if (status === "D") stats.filesDeleted++;
542
+ else if (status?.startsWith("R")) {
543
+ stats.filesUpdated++;
544
+ stats.filesRenamed++;
545
+ } else stats.filesUpdated++;
546
+ }
547
+ const numstat = git([
548
+ "diff",
549
+ "--numstat",
550
+ range
551
+ ], cwd);
552
+ for (const line of numstat.split("\n")) {
553
+ if (!line) continue;
554
+ const [added, deleted] = line.split(" ");
555
+ if (added === "-" || deleted === "-") {
556
+ stats.binaryFiles++;
557
+ continue;
558
+ }
559
+ stats.linesAdded += Number.parseInt(added ?? "0", 10) || 0;
560
+ stats.linesDeleted += Number.parseInt(deleted ?? "0", 10) || 0;
561
+ }
562
+ return stats;
563
+ }
482
564
  function commitAll(message, cwd) {
483
565
  git(["add", "-A"], cwd);
484
566
  try {
@@ -660,7 +742,7 @@ function resolveConventionalScope(value) {
660
742
  return scope === "" ? "" : `(${scope})`;
661
743
  }
662
744
  function buildCommitMessage(config, output, context) {
663
- if (config === void 0) return collapseHeader(`gnhf #${context.iteration}: ${output.summary}`);
745
+ if (config === void 0) return collapseHeader(`gnhf ${context.iteration}: ${output.summary}`);
664
746
  const commitOutput = output;
665
747
  return collapseHeader(`${resolveConventionalType(commitOutput.type)}${resolveConventionalScope(commitOutput.scope)}: ${output.summary}`);
666
748
  }
@@ -13461,6 +13543,40 @@ function isAbortError$2(error) {
13461
13543
  function createAbortError$2() {
13462
13544
  return /* @__PURE__ */ new Error("Agent was aborted");
13463
13545
  }
13546
+ function redactRawAcpTargetInString(text, target) {
13547
+ const redacted = redactAcpTargetForLogs(target);
13548
+ if (redacted === target) return text;
13549
+ return text.split(target).join(redacted);
13550
+ }
13551
+ function redactRawAcpTargetInValue(value, target) {
13552
+ if (typeof value === "string") return redactRawAcpTargetInString(value, target);
13553
+ if (Array.isArray(value)) return value.map((item) => redactRawAcpTargetInValue(item, target));
13554
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactRawAcpTargetInValue(entry, target)]));
13555
+ return value;
13556
+ }
13557
+ function serializeAcpErrorForLog(error, target) {
13558
+ return redactRawAcpTargetInValue(serializeError(error), target);
13559
+ }
13560
+ function redactAcpErrorForThrow(error, target) {
13561
+ if (redactAcpTargetForLogs(target) === target) return error;
13562
+ if (error instanceof PermanentAgentError) return new PermanentAgentError(redactRawAcpTargetInString(error.message, target), redactRawAcpTargetInString(error.detail, target));
13563
+ if (error instanceof Error) {
13564
+ let cause;
13565
+ try {
13566
+ cause = "cause" in error ? error.cause : void 0;
13567
+ } catch {
13568
+ cause = void 0;
13569
+ }
13570
+ const redactedCause = cause === void 0 ? void 0 : redactAcpErrorForThrow(cause, target);
13571
+ const redactedError = new Error(redactRawAcpTargetInString(error.message, target), redactedCause === void 0 ? void 0 : { cause: redactedCause });
13572
+ redactedError.name = error.name;
13573
+ if (typeof error.stack === "string") redactedError.stack = redactRawAcpTargetInString(error.stack, target);
13574
+ const code = error.code;
13575
+ if (code !== void 0) redactedError.code = code;
13576
+ return redactedError;
13577
+ }
13578
+ return redactRawAcpTargetInValue(error, target);
13579
+ }
13464
13580
  function estimateTokens(charCount) {
13465
13581
  if (charCount <= 0) return 0;
13466
13582
  return Math.ceil(charCount / 4);
@@ -13493,30 +13609,47 @@ var AcpAgent = class {
13493
13609
  const { signal, onMessage, onUsage, logPath } = options ?? {};
13494
13610
  if (signal?.aborted) throw createAbortError$2();
13495
13611
  const runtime = this.ensureRuntime(cwd);
13496
- const handle = await runtime.ensureSession({
13497
- sessionKey: this.runId,
13498
- agent: this.target,
13499
- mode: "persistent",
13500
- cwd
13501
- });
13612
+ let handle;
13613
+ try {
13614
+ handle = await runtime.ensureSession({
13615
+ sessionKey: this.runId,
13616
+ agent: this.target,
13617
+ mode: "persistent",
13618
+ cwd
13619
+ });
13620
+ } catch (error) {
13621
+ throw redactAcpErrorForThrow(error, this.target);
13622
+ }
13502
13623
  this.handle = handle;
13503
13624
  const requestId = randomUUID();
13504
13625
  appendDebugLog("acp:turn:start", {
13505
- target: this.target,
13626
+ target: redactAcpTargetForLogs(this.target),
13506
13627
  sessionKey: this.runId,
13507
13628
  requestId,
13508
13629
  cwd
13509
13630
  });
13510
13631
  const acpPrompt = buildAcpPrompt(prompt, this.schema);
13511
13632
  const promptTokenEstimate = estimateTokens(acpPrompt.length);
13512
- const turn = runtime.startTurn({
13513
- handle,
13514
- text: acpPrompt,
13515
- mode: "prompt",
13516
- requestId,
13517
- signal
13518
- });
13519
13633
  const startedAt = Date.now();
13634
+ const turn = (() => {
13635
+ try {
13636
+ return runtime.startTurn({
13637
+ handle,
13638
+ text: acpPrompt,
13639
+ mode: "prompt",
13640
+ requestId,
13641
+ signal
13642
+ });
13643
+ } catch (error) {
13644
+ appendDebugLog("acp:turn:start-error", {
13645
+ target: redactAcpTargetForLogs(this.target),
13646
+ requestId,
13647
+ elapsedMs: Date.now() - startedAt,
13648
+ error: serializeAcpErrorForLog(error, this.target)
13649
+ });
13650
+ throw redactAcpErrorForThrow(error, this.target);
13651
+ }
13652
+ })();
13520
13653
  const iterationStartUsed = this.lastReportedUsed;
13521
13654
  let latestUsed = iterationStartUsed;
13522
13655
  let usageUpdateReceived = iterationStartUsed > 0;
@@ -13587,23 +13720,23 @@ var AcpAgent = class {
13587
13720
  if (signal?.aborted || isAbortError$2(error)) {
13588
13721
  await turn.cancel({ reason: "gnhf-aborted" }).catch(() => void 0);
13589
13722
  appendDebugLog("acp:turn:aborted", {
13590
- target: this.target,
13723
+ target: redactAcpTargetForLogs(this.target),
13591
13724
  requestId,
13592
13725
  elapsedMs: Date.now() - startedAt
13593
13726
  });
13594
13727
  throw createAbortError$2();
13595
13728
  }
13596
13729
  appendDebugLog("acp:turn:stream-error", {
13597
- target: this.target,
13730
+ target: redactAcpTargetForLogs(this.target),
13598
13731
  requestId,
13599
13732
  elapsedMs: Date.now() - startedAt,
13600
- error: serializeError(error)
13733
+ error: serializeAcpErrorForLog(error, this.target)
13601
13734
  });
13602
- throw error;
13735
+ throw redactAcpErrorForThrow(error, this.target);
13603
13736
  }
13604
13737
  const result = await turn.result;
13605
13738
  appendDebugLog("acp:turn:result", {
13606
- target: this.target,
13739
+ target: redactAcpTargetForLogs(this.target),
13607
13740
  requestId,
13608
13741
  status: result.status,
13609
13742
  stopReason: result.status === "completed" || result.status === "cancelled" ? result.stopReason : void 0,
@@ -13614,7 +13747,7 @@ var AcpAgent = class {
13614
13747
  });
13615
13748
  if (result.status === "cancelled") throw createAbortError$2();
13616
13749
  if (result.status === "failed") {
13617
- const message = result.error.message || "ACP turn failed";
13750
+ const message = redactRawAcpTargetInString(result.error.message || "ACP turn failed", this.target);
13618
13751
  if (result.error.retryable === false) throw new PermanentAgentError(message, result.error.code ?? "ACP_TURN_FAILED");
13619
13752
  throw new Error(message);
13620
13753
  }
@@ -13658,11 +13791,11 @@ var AcpAgent = class {
13658
13791
  handle,
13659
13792
  reason: "gnhf-shutdown"
13660
13793
  });
13661
- appendDebugLog("acp:close", { target: this.target });
13794
+ appendDebugLog("acp:close", { target: redactAcpTargetForLogs(this.target) });
13662
13795
  } catch (error) {
13663
13796
  appendDebugLog("acp:close-error", {
13664
- target: this.target,
13665
- error: serializeError(error)
13797
+ target: redactAcpTargetForLogs(this.target),
13798
+ error: serializeAcpErrorForLog(error, this.target)
13666
13799
  });
13667
13800
  }
13668
13801
  }
@@ -13677,7 +13810,7 @@ var AcpAgent = class {
13677
13810
  });
13678
13811
  this.runtime = runtime;
13679
13812
  appendDebugLog("acp:runtime:created", {
13680
- target: this.target,
13813
+ target: redactAcpTargetForLogs(this.target),
13681
13814
  sessionStateDir: this.sessionStateDir,
13682
13815
  cwd
13683
13816
  });
@@ -16343,7 +16476,7 @@ var Orchestrator = class extends EventEmitter {
16343
16476
  this.state.status = "running";
16344
16477
  this.emit("state", this.getState());
16345
16478
  appendDebugLog("orchestrator:start", {
16346
- agent: this.agent.name,
16479
+ agent: redactAgentSpecForLogs(this.agent.name),
16347
16480
  runId: this.runInfo.runId,
16348
16481
  startIteration: this.state.currentIteration,
16349
16482
  maxIterations: this.limits.maxIterations,
@@ -16502,7 +16635,7 @@ var Orchestrator = class extends EventEmitter {
16502
16635
  const agentStartedAt = Date.now();
16503
16636
  appendDebugLog("agent:run:start", {
16504
16637
  iteration: this.state.currentIteration,
16505
- agent: this.agent.name,
16638
+ agent: redactAgentSpecForLogs(this.agent.name),
16506
16639
  logPath
16507
16640
  });
16508
16641
  try {
@@ -16707,6 +16840,158 @@ var Orchestrator = class extends EventEmitter {
16707
16840
  }
16708
16841
  };
16709
16842
  //#endregion
16843
+ //#region src/utils/tokens.ts
16844
+ function formatTokens(count) {
16845
+ if (count >= 0xe8d4a51000) return `${(count / 0xe8d4a51000).toFixed(1)}T`;
16846
+ if (count >= 1e9) return `${(count / 1e9).toFixed(1)}B`;
16847
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
16848
+ if (count >= 1e3) return `${Math.round(count / 1e3)}K`;
16849
+ return String(count);
16850
+ }
16851
+ //#endregion
16852
+ //#region src/core/exit-summary.ts
16853
+ const MIN_CARD_WIDTH = 62;
16854
+ const LABEL_WIDTH = 16;
16855
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
16856
+ const ANSI_TOKEN_RE = /\x1b\[[0-9;]*m/g;
16857
+ const NO_MISTAKES_URL = "https://github.com/kunchenguid/no-mistakes";
16858
+ function stripExitSummaryAnsi(text) {
16859
+ return text.replace(ANSI_RE, "");
16860
+ }
16861
+ function makeStyles(color) {
16862
+ const wrap = (open, text) => color ? `${open}${text}\x1b[0m` : text;
16863
+ return {
16864
+ dim: (text) => wrap("\x1B[2m", text),
16865
+ bold: (text) => wrap("\x1B[1m", text),
16866
+ cyan: (text) => wrap("\x1B[36m", text),
16867
+ yellow: (text) => wrap("\x1B[33m", text),
16868
+ green: (text) => wrap("\x1B[32m", text),
16869
+ red: (text) => wrap("\x1B[31m", text),
16870
+ magenta: (text) => wrap("\x1B[35m", text),
16871
+ blueUnderline: (text) => wrap("\x1B[34;4m", text)
16872
+ };
16873
+ }
16874
+ function visibleLength(text) {
16875
+ return stripExitSummaryAnsi(text).length;
16876
+ }
16877
+ function padVisible(text, width) {
16878
+ return text + " ".repeat(Math.max(0, width - visibleLength(text)));
16879
+ }
16880
+ function truncateVisible(text, width) {
16881
+ if (visibleLength(text) <= width) return text;
16882
+ if (width <= 0) return "";
16883
+ const targetWidth = Math.max(0, width - 1);
16884
+ let output = "";
16885
+ let visible = 0;
16886
+ let index = 0;
16887
+ let hasActiveStyle = false;
16888
+ const finish = () => `${output}…${hasActiveStyle ? "\x1B[0m" : ""}`;
16889
+ for (const match of text.matchAll(ANSI_TOKEN_RE)) {
16890
+ const chunk = text.slice(index, match.index);
16891
+ for (const char of chunk) {
16892
+ if (visible >= targetWidth) return finish();
16893
+ output += char;
16894
+ visible += 1;
16895
+ }
16896
+ output += match[0];
16897
+ hasActiveStyle = match[0] !== "\x1B[0m";
16898
+ index = match.index + match[0].length;
16899
+ }
16900
+ for (const char of text.slice(index)) {
16901
+ if (visible >= targetWidth) return finish();
16902
+ output += char;
16903
+ visible += 1;
16904
+ }
16905
+ return finish();
16906
+ }
16907
+ function formatDuration(ms) {
16908
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
16909
+ const hours = Math.floor(totalSeconds / 3600);
16910
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
16911
+ const seconds = totalSeconds % 60;
16912
+ if (hours > 0) return `${hours}h ${minutes}m`;
16913
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
16914
+ return `${seconds}s`;
16915
+ }
16916
+ function formatNumber(value) {
16917
+ return new Intl.NumberFormat("en-US").format(value);
16918
+ }
16919
+ function formatTokenCount$1(value, suffix, estimated) {
16920
+ return `${estimated ? "~" : ""}${formatTokens(value)} ${suffix}`;
16921
+ }
16922
+ function plural(value, singular, pluralText = `${singular}s`) {
16923
+ return `${formatNumber(value)} ${value === 1 ? singular : pluralText}`;
16924
+ }
16925
+ function metricLine(label, columns) {
16926
+ return ` ${padVisible(`${padVisible(label, LABEL_WIDTH)}${columns[0] ?? ""}`, 30)}${padVisible(columns[1] ?? "", 13)}${columns[2] ?? ""}`;
16927
+ }
16928
+ function commandLine(label, command) {
16929
+ return ` ${padVisible(label, LABEL_WIDTH)}${command}`;
16930
+ }
16931
+ function continuationLine(text) {
16932
+ return ` ${"".padEnd(LABEL_WIDTH)}${text}`;
16933
+ }
16934
+ function resolveCardWidth(contents, terminalColumns) {
16935
+ const contentWidth = Math.max(...contents.map(visibleLength));
16936
+ const desiredWidth = Math.max(MIN_CARD_WIDTH, contentWidth + 4);
16937
+ const columns = terminalColumns && terminalColumns > 0 ? terminalColumns : void 0;
16938
+ return Math.max(4, columns ? Math.min(desiredWidth, columns) : desiredWidth);
16939
+ }
16940
+ function cardBorder(left, right, width, dim) {
16941
+ return dim(`${left}${"─".repeat(Math.max(0, width - 2))}${right}`);
16942
+ }
16943
+ function cardLine(content, width, dim) {
16944
+ const contentWidth = Math.max(0, width - 4);
16945
+ return `${dim("│ ")}${padVisible(truncateVisible(content, contentWidth), contentWidth)}${dim(" │")}`;
16946
+ }
16947
+ function renderExitSummary(options) {
16948
+ const s = makeStyles(options.color);
16949
+ const elapsed = formatDuration(options.elapsedMs);
16950
+ const stopped = options.status === "aborted";
16951
+ const title = stopped ? `${s.red("×")} ${s.bold("gnhf stopped")}` : `${s.cyan("✦")} ${s.bold("gnhf wrapped")}`;
16952
+ const subtitle = stopped ? `${s.cyan(options.agentName)} ran for ${s.yellow(elapsed)} before: ${options.abortReason ?? options.status}` : `${s.cyan(options.agentName)} worked for ${s.yellow(elapsed)} on ${s.magenta(options.branchName)}`;
16953
+ const cardWidth = resolveCardWidth([title, ` ${subtitle}`], options.terminalColumns);
16954
+ const rolledBack = `${options.failCount} rolled back`;
16955
+ const inputTokens = formatTokenCount$1(options.totalInputTokens, "in", options.tokensEstimated);
16956
+ const outputTokens = formatTokenCount$1(options.totalOutputTokens, "out", options.tokensEstimated);
16957
+ const commits = plural(options.commitCount, "commit");
16958
+ const linesAdded = `+${formatNumber(options.diffStats.linesAdded)}`;
16959
+ const linesDeleted = `-${formatNumber(options.diffStats.linesDeleted)}`;
16960
+ return `\n${[
16961
+ cardBorder("╭", "╮", cardWidth, s.dim),
16962
+ cardLine(title, cardWidth, s.dim),
16963
+ cardLine(` ${subtitle}`, cardWidth, s.dim),
16964
+ cardBorder("╰", "╯", cardWidth, s.dim),
16965
+ "",
16966
+ metricLine(s.dim("iterations"), [
16967
+ `${s.bold(String(options.iterations))} total`,
16968
+ s.green(`${options.successCount} good`),
16969
+ stopped ? s.red(rolledBack) : s.yellow(rolledBack)
16970
+ ]),
16971
+ metricLine(s.dim("tokens"), [s.bold(inputTokens), s.bold(outputTokens)]),
16972
+ metricLine(s.dim("branch diff"), [
16973
+ s.bold(commits),
16974
+ s.green(linesAdded),
16975
+ s.red(linesDeleted)
16976
+ ]),
16977
+ metricLine(s.dim("files"), [
16978
+ `${options.diffStats.filesAdded} added`,
16979
+ `${options.diffStats.filesUpdated} updated`,
16980
+ `${options.diffStats.filesDeleted} deleted`
16981
+ ]),
16982
+ "",
16983
+ commandLine(s.dim("notes"), options.notesPath),
16984
+ commandLine(s.dim("debug log"), options.logPath),
16985
+ "",
16986
+ commandLine(s.dim("next steps"), s.cyan(`git log --oneline ${options.baseRef}..HEAD`)),
16987
+ continuationLine(s.cyan(`git diff --stat ${options.baseRef}..HEAD`)),
16988
+ continuationLine(s.cyan("gh pr create")),
16989
+ "",
16990
+ commandLine(s.dim("too much"), `${s.cyan("git push no-mistakes")}:`),
16991
+ commandLine(s.dim("to review?"), s.blueUnderline(NO_MISTAKES_URL))
16992
+ ].join("\n")}\n`;
16993
+ }
16994
+ //#endregion
16710
16995
  //#region src/mock-orchestrator.ts
16711
16996
  function mockIter(n, success, summary, agoMs) {
16712
16997
  return {
@@ -16920,15 +17205,6 @@ function formatElapsed(ms) {
16920
17205
  return `${String(Math.floor(s / 3600)).padStart(2, "0")}:${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
16921
17206
  }
16922
17207
  //#endregion
16923
- //#region src/utils/tokens.ts
16924
- function formatTokens(count) {
16925
- if (count >= 0xe8d4a51000) return `${(count / 0xe8d4a51000).toFixed(1)}T`;
16926
- if (count >= 1e9) return `${(count / 1e9).toFixed(1)}B`;
16927
- if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
16928
- if (count >= 1e3) return `${Math.round(count / 1e3)}K`;
16929
- return String(count);
16930
- }
16931
- //#endregion
16932
17208
  //#region src/utils/terminal-width.ts
16933
17209
  const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
16934
17210
  const MARK_REGEX = /\p{Mark}/u;
@@ -17548,7 +17824,7 @@ const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
17548
17824
  const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
17549
17825
  const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
17550
17826
  const AGENT_NAME_SET = new Set(AGENT_NAMES);
17551
- const AGENT_SPEC_LIST = `${`"${AGENT_NAMES.slice(0, -1).join("\", \"")}", or "${AGENT_NAMES[AGENT_NAMES.length - 1]}"`}, or "acp:<target>" (e.g. acp:gemini)`;
17827
+ const AGENT_SPEC_LIST = `${`"${AGENT_NAMES.slice(0, -1).join("\", \"")}", or "${AGENT_NAMES[AGENT_NAMES.length - 1]}"`}, or "acp:<target-or-command>" (e.g. acp:gemini)`;
17552
17828
  var PromptSignalError = class extends Error {
17553
17829
  constructor(signal) {
17554
17830
  super(signal);
@@ -17576,6 +17852,42 @@ function isAgentName(name) {
17576
17852
  function getNativeAgentName(spec) {
17577
17853
  return isAgentName(spec) ? spec : void 0;
17578
17854
  }
17855
+ function getTelemetryAgent(spec) {
17856
+ return redactAgentSpecForLogs(spec);
17857
+ }
17858
+ function shouldUseColor() {
17859
+ return process$1.stdout.isTTY === true && process$1.env.NO_COLOR === void 0 && process$1.env.TERM !== "dumb";
17860
+ }
17861
+ function emptyBranchDiffStats(commitCount) {
17862
+ return {
17863
+ commits: commitCount,
17864
+ filesChanged: 0,
17865
+ filesAdded: 0,
17866
+ filesUpdated: 0,
17867
+ filesDeleted: 0,
17868
+ filesRenamed: 0,
17869
+ binaryFiles: 0,
17870
+ linesAdded: 0,
17871
+ linesDeleted: 0
17872
+ };
17873
+ }
17874
+ function redactDebugArgs(args) {
17875
+ const redacted = [...args];
17876
+ for (let i = 0; i < redacted.length; i += 1) {
17877
+ const arg = redacted[i];
17878
+ if (arg === "--") break;
17879
+ if (arg === "--agent") {
17880
+ const next = redacted[i + 1];
17881
+ if (next !== void 0) {
17882
+ redacted[i + 1] = redactAgentSpecForLogs(next);
17883
+ i += 1;
17884
+ }
17885
+ continue;
17886
+ }
17887
+ if (arg?.startsWith("--agent=")) redacted[i] = `--agent=${redactAgentSpecForLogs(arg.slice(8))}`;
17888
+ }
17889
+ return redacted;
17890
+ }
17579
17891
  function buildSchemaOptions(stopWhen, commitMessage) {
17580
17892
  const commitFields = getCommitMessageSchemaFields(commitMessage);
17581
17893
  return {
@@ -17797,7 +18109,7 @@ function readReexecStdinPrompt(env) {
17797
18109
  }
17798
18110
  }
17799
18111
  const program = new Command();
17800
- 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>)`).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; 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("--mock", "", false).action(async (promptArg, options) => {
18112
+ 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; 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("--mock", "", false).action(async (promptArg, options) => {
17801
18113
  if (options.mock) {
17802
18114
  const mock = new MockOrchestrator();
17803
18115
  enterAltScreen();
@@ -17939,16 +18251,17 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
17939
18251
  }
17940
18252
  }
17941
18253
  const runMode = options.worktree ? "worktree" : startIteration > 0 ? "resume" : "new";
18254
+ const telemetryAgent = getTelemetryAgent(config.agent);
17942
18255
  telemetry.pageview("/run", {
17943
- agent: config.agent,
18256
+ agent: telemetryAgent,
17944
18257
  mode: runMode
17945
18258
  });
17946
18259
  initDebugLog(runInfo.logPath);
17947
18260
  appendDebugLog("run:start", {
17948
- args: process$1.argv.slice(2),
18261
+ args: redactDebugArgs(process$1.argv.slice(2)),
17949
18262
  runId: runInfo.runId,
17950
18263
  runDir: runInfo.runDir,
17951
- agent: config.agent,
18264
+ agent: redactAgentSpecForLogs(config.agent),
17952
18265
  promptLength: prompt.length,
17953
18266
  promptFromStdin,
17954
18267
  startIteration,
@@ -18037,6 +18350,38 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18037
18350
  }
18038
18351
  {
18039
18352
  const finalState = orchestrator.getState();
18353
+ let finalBranchName = "HEAD";
18354
+ try {
18355
+ finalBranchName = getCurrentBranch(effectiveCwd);
18356
+ } catch (error) {
18357
+ appendDebugLog("summary:branch-error", { error: serializeError(error) });
18358
+ }
18359
+ let diffStats = emptyBranchDiffStats(finalState.commitCount);
18360
+ try {
18361
+ diffStats = getBranchDiffStats(runInfo.baseCommit, effectiveCwd);
18362
+ } catch (error) {
18363
+ appendDebugLog("summary:diff-stats-error", { error: serializeError(error) });
18364
+ }
18365
+ const exitSummary = renderExitSummary({
18366
+ agentName: redactAgentSpecForLogs(config.agent),
18367
+ branchName: finalBranchName,
18368
+ elapsedMs: Date.now() - finalState.startTime.getTime(),
18369
+ status: finalState.status,
18370
+ abortReason: finalState.lastAgentError ?? finalState.lastMessage,
18371
+ iterations: finalState.currentIteration,
18372
+ successCount: finalState.successCount,
18373
+ failCount: finalState.failCount,
18374
+ totalInputTokens: finalState.totalInputTokens,
18375
+ totalOutputTokens: finalState.totalOutputTokens,
18376
+ tokensEstimated: finalState.tokensEstimated,
18377
+ commitCount: finalState.commitCount,
18378
+ notesPath: runInfo.notesPath,
18379
+ logPath: runInfo.logPath,
18380
+ baseRef: runInfo.baseCommit.slice(0, 12) || runInfo.baseCommit,
18381
+ diffStats,
18382
+ color: shouldUseColor(),
18383
+ terminalColumns: process$1.stdout.columns
18384
+ });
18040
18385
  appendDebugLog("run:complete", {
18041
18386
  signal: shutdownSignal,
18042
18387
  status: finalState.status,
@@ -18049,7 +18394,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18049
18394
  worktreePath
18050
18395
  });
18051
18396
  telemetry.track("run", {
18052
- agent: config.agent,
18397
+ agent: telemetryAgent,
18053
18398
  mode: runMode,
18054
18399
  status: finalState.status,
18055
18400
  signal: shutdownSignal ?? void 0,
@@ -18074,6 +18419,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18074
18419
  worktreeCleanup = null;
18075
18420
  appendDebugLog("worktree:cleaned-up", { worktreePath });
18076
18421
  }
18422
+ process$1.stdout.write(exitSummary);
18077
18423
  }
18078
18424
  if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
18079
18425
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "@types/js-yaml": "^4.0.9",
28
28
  "@types/node": "^22.0.0",
29
29
  "@vitest/coverage-v8": "^4.1.2",
30
+ "acp-mock": "^1.1.0",
30
31
  "acpx": "^0.6.1",
31
32
  "eslint": "^9.0.0",
32
33
  "eslint-config-prettier": "^10.0.0",