gnhf 0.1.9 → 0.1.11
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 +17 -0
- package/dist/cli.mjs +411 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,11 @@ Config lives at `~/.gnhf/config.yml`:
|
|
|
160
160
|
# Agent to use by default (claude, codex, rovodev, or opencode)
|
|
161
161
|
agent: claude
|
|
162
162
|
|
|
163
|
+
# Custom paths to agent binaries (optional)
|
|
164
|
+
# agentPathOverride:
|
|
165
|
+
# claude: /path/to/custom-claude
|
|
166
|
+
# codex: /path/to/custom-codex
|
|
167
|
+
|
|
163
168
|
# Abort after this many consecutive failures
|
|
164
169
|
maxConsecutiveFailures: 3
|
|
165
170
|
|
|
@@ -171,6 +176,18 @@ If the file does not exist yet, `gnhf` creates it on first run using the resolve
|
|
|
171
176
|
|
|
172
177
|
CLI flags override config file values. `--prevent-sleep` accepts `on`/`off` as well as `true`/`false`; the config file always uses a boolean.
|
|
173
178
|
The iteration and token caps are runtime-only flags and are not persisted in `config.yml`.
|
|
179
|
+
|
|
180
|
+
### Custom Agent Paths
|
|
181
|
+
|
|
182
|
+
Use `agentPathOverride` to point any agent at a custom binary — useful for wrappers like Claude Code Switch or custom Codex builds that accept the same flags and arguments as the original:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
agentPathOverride:
|
|
186
|
+
claude: ~/bin/claude-code-switch
|
|
187
|
+
codex: /usr/local/bin/my-codex-wrapper
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Paths may be absolute, bare executable names already on your `PATH`, `~`-prefixed, or relative to the config directory (`~/.gnhf/`). The override replaces only the binary name; all standard arguments are preserved, so the replacement must be CLI-compatible with the original agent. On Windows, `.cmd` and `.bat` wrappers are supported, including bare names resolved from `PATH`. For `rovodev`, the override must point to an `acli`-compatible binary since gnhf invokes it as `<bin> rovodev serve ...`.
|
|
174
191
|
When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS: `caffeinate` on macOS, `systemd-inhibit` on Linux, and a small PowerShell helper backed by `SetThreadExecutionState` on Windows.
|
|
175
192
|
|
|
176
193
|
## Debug Logs
|
package/dist/cli.mjs
CHANGED
|
@@ -11,8 +11,15 @@ import { createServer } from "node:net";
|
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
//#region src/core/config.ts
|
|
14
|
+
const AGENT_NAMES = [
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"rovodev",
|
|
18
|
+
"opencode"
|
|
19
|
+
];
|
|
14
20
|
const DEFAULT_CONFIG = {
|
|
15
21
|
agent: "claude",
|
|
22
|
+
agentPathOverride: {},
|
|
16
23
|
maxConsecutiveFailures: 3,
|
|
17
24
|
preventSleep: true
|
|
18
25
|
};
|
|
@@ -25,7 +32,35 @@ function normalizePreventSleep(value) {
|
|
|
25
32
|
if (value === "on") return true;
|
|
26
33
|
if (value === "off") return false;
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a user-supplied path against the config directory (~/.gnhf).
|
|
37
|
+
* Expands leading `~` or `~/` to the home directory, then resolves relative
|
|
38
|
+
* paths against `baseDir` so that entries like `./bin/codex` work predictably
|
|
39
|
+
* regardless of the repo's cwd. Bare executable names and absolute paths pass
|
|
40
|
+
* through unchanged.
|
|
41
|
+
*/
|
|
42
|
+
function resolveConfigPath(raw, baseDir) {
|
|
43
|
+
if (raw !== "~" && !raw.startsWith("~/") && !raw.startsWith("~\\") && !raw.includes("/") && !raw.includes("\\")) return raw;
|
|
44
|
+
const home = homedir();
|
|
45
|
+
let expanded = raw;
|
|
46
|
+
if (expanded === "~") expanded = home;
|
|
47
|
+
else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) expanded = join(home, expanded.slice(2));
|
|
48
|
+
return resolve(baseDir, expanded);
|
|
49
|
+
}
|
|
50
|
+
function normalizeAgentPathOverride(value, configDir) {
|
|
51
|
+
if (value === void 0 || value === null) return void 0;
|
|
52
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new InvalidConfigError(`Invalid config value for agentPathOverride: expected an object mapping agent names to paths`);
|
|
53
|
+
const validNames = new Set(AGENT_NAMES);
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, val] of Object.entries(value)) {
|
|
56
|
+
if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentPathOverride: "${key}". Use "claude", "codex", "rovodev", or "opencode".`);
|
|
57
|
+
if (typeof val !== "string") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a string`);
|
|
58
|
+
if (val.trim() === "") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a non-empty string`);
|
|
59
|
+
result[key] = resolveConfigPath(val, configDir);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function normalizeConfig(config, configDir) {
|
|
29
64
|
const normalized = { ...config };
|
|
30
65
|
const hasPreventSleep = Object.prototype.hasOwnProperty.call(config, "preventSleep");
|
|
31
66
|
const preventSleep = normalizePreventSleep(config.preventSleep);
|
|
@@ -33,22 +68,47 @@ function normalizeConfig(config) {
|
|
|
33
68
|
if (hasPreventSleep && config.preventSleep !== void 0) throw new InvalidConfigError(`Invalid config value for preventSleep: ${String(config.preventSleep)}`);
|
|
34
69
|
delete normalized.preventSleep;
|
|
35
70
|
} else normalized.preventSleep = preventSleep;
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(config, "agentPathOverride")) {
|
|
72
|
+
const resolveDir = configDir ?? join(homedir(), ".gnhf");
|
|
73
|
+
const agentPathOverride = normalizeAgentPathOverride(config.agentPathOverride, resolveDir);
|
|
74
|
+
if (agentPathOverride === void 0) delete normalized.agentPathOverride;
|
|
75
|
+
else normalized.agentPathOverride = agentPathOverride;
|
|
76
|
+
} else delete normalized.agentPathOverride;
|
|
36
77
|
return normalized;
|
|
37
78
|
}
|
|
38
79
|
function isMissingConfigError(error) {
|
|
39
80
|
if (!(error instanceof Error)) return false;
|
|
40
81
|
return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
|
|
41
82
|
}
|
|
83
|
+
function serializeAgentPathOverride(agentPathOverride) {
|
|
84
|
+
const serializedOverrides = Object.fromEntries(AGENT_NAMES.flatMap((name) => {
|
|
85
|
+
const value = agentPathOverride[name];
|
|
86
|
+
return value === void 0 ? [] : [[name, value]];
|
|
87
|
+
}));
|
|
88
|
+
if (Object.keys(serializedOverrides).length === 0) return "";
|
|
89
|
+
return yaml.dump({ agentPathOverride: serializedOverrides }, {
|
|
90
|
+
lineWidth: -1,
|
|
91
|
+
noRefs: true,
|
|
92
|
+
sortKeys: false
|
|
93
|
+
}).trimEnd();
|
|
94
|
+
}
|
|
42
95
|
function serializeConfig(config) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
const agentPathOverrideSection = serializeAgentPathOverride(config.agentPathOverride);
|
|
97
|
+
const lines = [
|
|
98
|
+
"# Agent to use by default",
|
|
99
|
+
`agent: ${config.agent}`,
|
|
100
|
+
"",
|
|
101
|
+
"# Custom paths to agent binaries (optional)",
|
|
102
|
+
"# Paths may be absolute, bare executable names on PATH,",
|
|
103
|
+
"# ~-prefixed, or relative to this config directory.",
|
|
104
|
+
"# Note: rovodev overrides must point to an acli-compatible binary.",
|
|
105
|
+
"# agentPathOverride:",
|
|
106
|
+
"# claude: /path/to/custom-claude",
|
|
107
|
+
"# codex: /path/to/custom-codex"
|
|
108
|
+
];
|
|
109
|
+
if (agentPathOverrideSection) lines.push(...agentPathOverrideSection.split("\n"));
|
|
110
|
+
lines.push("", "# Abort after this many consecutive failures", `maxConsecutiveFailures: ${config.maxConsecutiveFailures}`, "", "# Prevent the machine from sleeping during a run", `preventSleep: ${config.preventSleep}`, "");
|
|
111
|
+
return lines.join("\n");
|
|
52
112
|
}
|
|
53
113
|
function loadConfig(overrides) {
|
|
54
114
|
const configDir = join(homedir(), ".gnhf");
|
|
@@ -57,7 +117,7 @@ function loadConfig(overrides) {
|
|
|
57
117
|
let shouldBootstrapConfig = false;
|
|
58
118
|
try {
|
|
59
119
|
const raw = readFileSync(configPath, "utf-8");
|
|
60
|
-
fileConfig = normalizeConfig(yaml.load(raw) ?? {});
|
|
120
|
+
fileConfig = normalizeConfig(yaml.load(raw) ?? {}, configDir);
|
|
61
121
|
} catch (error) {
|
|
62
122
|
if (error instanceof InvalidConfigError) throw error;
|
|
63
123
|
if (isMissingConfigError(error)) shouldBootstrapConfig = true;
|
|
@@ -745,10 +805,12 @@ function parseJSONLStream(stream, logStream, callback) {
|
|
|
745
805
|
* Wire an AbortSignal to kill a child process.
|
|
746
806
|
* Returns true if the signal was already aborted (caller should return early).
|
|
747
807
|
*/
|
|
748
|
-
function setupAbortHandler(signal, child, reject) {
|
|
808
|
+
function setupAbortHandler(signal, child, reject, abortChild = () => {
|
|
809
|
+
child.kill("SIGTERM");
|
|
810
|
+
}) {
|
|
749
811
|
if (!signal) return false;
|
|
750
812
|
const onAbort = () => {
|
|
751
|
-
|
|
813
|
+
abortChild();
|
|
752
814
|
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
753
815
|
};
|
|
754
816
|
if (signal.aborted) {
|
|
@@ -761,13 +823,52 @@ function setupAbortHandler(signal, child, reject) {
|
|
|
761
823
|
}
|
|
762
824
|
//#endregion
|
|
763
825
|
//#region src/core/agents/claude.ts
|
|
826
|
+
function shouldUseWindowsShell$2(bin, platform) {
|
|
827
|
+
if (platform !== "win32") return false;
|
|
828
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
829
|
+
if (/[\\/]/.test(bin)) return false;
|
|
830
|
+
try {
|
|
831
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
832
|
+
encoding: "utf8",
|
|
833
|
+
stdio: [
|
|
834
|
+
"ignore",
|
|
835
|
+
"pipe",
|
|
836
|
+
"ignore"
|
|
837
|
+
]
|
|
838
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
839
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function terminateClaudeProcess(child, platform) {
|
|
845
|
+
if (platform === "win32" && child.pid) {
|
|
846
|
+
try {
|
|
847
|
+
execFileSync("taskkill", [
|
|
848
|
+
"/T",
|
|
849
|
+
"/F",
|
|
850
|
+
"/PID",
|
|
851
|
+
String(child.pid)
|
|
852
|
+
], { stdio: "ignore" });
|
|
853
|
+
} catch {}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
child.kill("SIGTERM");
|
|
857
|
+
}
|
|
764
858
|
var ClaudeAgent = class {
|
|
765
859
|
name = "claude";
|
|
860
|
+
bin;
|
|
861
|
+
platform;
|
|
862
|
+
constructor(binOrDeps = {}) {
|
|
863
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
864
|
+
this.bin = deps.bin ?? "claude";
|
|
865
|
+
this.platform = deps.platform ?? process.platform;
|
|
866
|
+
}
|
|
766
867
|
run(prompt, cwd, options) {
|
|
767
868
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
768
869
|
return new Promise((resolve, reject) => {
|
|
769
870
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
770
|
-
const child = spawn(
|
|
871
|
+
const child = spawn(this.bin, [
|
|
771
872
|
"-p",
|
|
772
873
|
prompt,
|
|
773
874
|
"--verbose",
|
|
@@ -778,6 +879,7 @@ var ClaudeAgent = class {
|
|
|
778
879
|
"--dangerously-skip-permissions"
|
|
779
880
|
], {
|
|
780
881
|
cwd,
|
|
882
|
+
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
781
883
|
stdio: [
|
|
782
884
|
"ignore",
|
|
783
885
|
"pipe",
|
|
@@ -785,7 +887,7 @@ var ClaudeAgent = class {
|
|
|
785
887
|
],
|
|
786
888
|
env: process.env
|
|
787
889
|
});
|
|
788
|
-
if (setupAbortHandler(signal, child, reject)) return;
|
|
890
|
+
if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
|
|
789
891
|
let resultEvent = null;
|
|
790
892
|
const cumulative = {
|
|
791
893
|
inputTokens: 0,
|
|
@@ -841,17 +943,54 @@ var ClaudeAgent = class {
|
|
|
841
943
|
};
|
|
842
944
|
//#endregion
|
|
843
945
|
//#region src/core/agents/codex.ts
|
|
946
|
+
function shouldUseWindowsShell$1(bin, platform) {
|
|
947
|
+
if (platform !== "win32") return false;
|
|
948
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
949
|
+
if (/[\\/]/.test(bin)) return false;
|
|
950
|
+
try {
|
|
951
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
952
|
+
encoding: "utf8",
|
|
953
|
+
stdio: [
|
|
954
|
+
"ignore",
|
|
955
|
+
"pipe",
|
|
956
|
+
"ignore"
|
|
957
|
+
]
|
|
958
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
959
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
960
|
+
} catch {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function terminateCodexProcess(child, platform) {
|
|
965
|
+
if (platform === "win32" && child.pid) {
|
|
966
|
+
try {
|
|
967
|
+
execFileSync("taskkill", [
|
|
968
|
+
"/T",
|
|
969
|
+
"/F",
|
|
970
|
+
"/PID",
|
|
971
|
+
String(child.pid)
|
|
972
|
+
], { stdio: "ignore" });
|
|
973
|
+
} catch {}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
child.kill("SIGTERM");
|
|
977
|
+
}
|
|
844
978
|
var CodexAgent = class {
|
|
845
979
|
name = "codex";
|
|
980
|
+
bin;
|
|
981
|
+
platform;
|
|
846
982
|
schemaPath;
|
|
847
|
-
constructor(schemaPath) {
|
|
983
|
+
constructor(schemaPath, binOrDeps = {}) {
|
|
984
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
985
|
+
this.bin = deps.bin ?? "codex";
|
|
986
|
+
this.platform = deps.platform ?? process.platform;
|
|
848
987
|
this.schemaPath = schemaPath;
|
|
849
988
|
}
|
|
850
989
|
run(prompt, cwd, options) {
|
|
851
990
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
852
991
|
return new Promise((resolve, reject) => {
|
|
853
992
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
854
|
-
const child = spawn(
|
|
993
|
+
const child = spawn(this.bin, [
|
|
855
994
|
"exec",
|
|
856
995
|
prompt,
|
|
857
996
|
"--json",
|
|
@@ -862,6 +1001,7 @@ var CodexAgent = class {
|
|
|
862
1001
|
"never"
|
|
863
1002
|
], {
|
|
864
1003
|
cwd,
|
|
1004
|
+
shell: shouldUseWindowsShell$1(this.bin, this.platform),
|
|
865
1005
|
stdio: [
|
|
866
1006
|
"ignore",
|
|
867
1007
|
"pipe",
|
|
@@ -869,7 +1009,7 @@ var CodexAgent = class {
|
|
|
869
1009
|
],
|
|
870
1010
|
env: process.env
|
|
871
1011
|
});
|
|
872
|
-
if (setupAbortHandler(signal, child, reject)) return;
|
|
1012
|
+
if (setupAbortHandler(signal, child, reject, () => terminateCodexProcess(child, this.platform))) return;
|
|
873
1013
|
let lastAgentMessage = null;
|
|
874
1014
|
const cumulative = {
|
|
875
1015
|
inputTokens: 0,
|
|
@@ -1018,6 +1158,7 @@ function withTimeoutSignal$1(signal, timeoutMs) {
|
|
|
1018
1158
|
}
|
|
1019
1159
|
var OpenCodeAgent = class {
|
|
1020
1160
|
name = "opencode";
|
|
1161
|
+
bin;
|
|
1021
1162
|
fetchFn;
|
|
1022
1163
|
getPortFn;
|
|
1023
1164
|
killProcessFn;
|
|
@@ -1026,6 +1167,7 @@ var OpenCodeAgent = class {
|
|
|
1026
1167
|
server = null;
|
|
1027
1168
|
closingPromise = null;
|
|
1028
1169
|
constructor(deps = {}) {
|
|
1170
|
+
this.bin = deps.bin ?? "opencode";
|
|
1029
1171
|
this.fetchFn = deps.fetch ?? fetch;
|
|
1030
1172
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1031
1173
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
@@ -1077,7 +1219,7 @@ var OpenCodeAgent = class {
|
|
|
1077
1219
|
const port = await this.getPortFn();
|
|
1078
1220
|
const isWindows = this.platform === "win32";
|
|
1079
1221
|
const detached = !isWindows;
|
|
1080
|
-
const child = this.spawnFn(
|
|
1222
|
+
const child = this.spawnFn(this.bin, [
|
|
1081
1223
|
"serve",
|
|
1082
1224
|
"--hostname",
|
|
1083
1225
|
"127.0.0.1",
|
|
@@ -1463,6 +1605,38 @@ function createAbortError() {
|
|
|
1463
1605
|
function isAbortError(error) {
|
|
1464
1606
|
return error instanceof Error && error.name === "AbortError";
|
|
1465
1607
|
}
|
|
1608
|
+
function shouldUseWindowsShell(bin, platform) {
|
|
1609
|
+
if (platform !== "win32") return false;
|
|
1610
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
1611
|
+
if (/[\\/]/.test(bin)) return false;
|
|
1612
|
+
try {
|
|
1613
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
1614
|
+
encoding: "utf8",
|
|
1615
|
+
stdio: [
|
|
1616
|
+
"ignore",
|
|
1617
|
+
"pipe",
|
|
1618
|
+
"ignore"
|
|
1619
|
+
]
|
|
1620
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1621
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
1622
|
+
} catch {
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function terminateRovoDevProcess(child, platform) {
|
|
1627
|
+
if (platform === "win32" && child.pid) {
|
|
1628
|
+
try {
|
|
1629
|
+
execFileSync("taskkill", [
|
|
1630
|
+
"/T",
|
|
1631
|
+
"/F",
|
|
1632
|
+
"/PID",
|
|
1633
|
+
String(child.pid)
|
|
1634
|
+
], { stdio: "ignore" });
|
|
1635
|
+
} catch {}
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
child.kill("SIGTERM");
|
|
1639
|
+
}
|
|
1466
1640
|
function getAvailablePort() {
|
|
1467
1641
|
return new Promise((resolve, reject) => {
|
|
1468
1642
|
const server = createServer();
|
|
@@ -1509,18 +1683,22 @@ async function delay(ms, signal) {
|
|
|
1509
1683
|
}
|
|
1510
1684
|
var RovoDevAgent = class {
|
|
1511
1685
|
name = "rovodev";
|
|
1686
|
+
bin;
|
|
1512
1687
|
schemaPath;
|
|
1513
1688
|
fetchFn;
|
|
1514
1689
|
getPortFn;
|
|
1515
1690
|
killProcessFn;
|
|
1691
|
+
platform;
|
|
1516
1692
|
spawnFn;
|
|
1517
1693
|
server = null;
|
|
1518
1694
|
closingPromise = null;
|
|
1519
1695
|
constructor(schemaPath, deps = {}) {
|
|
1696
|
+
this.bin = deps.bin ?? "acli";
|
|
1520
1697
|
this.schemaPath = schemaPath;
|
|
1521
1698
|
this.fetchFn = deps.fetch ?? fetch;
|
|
1522
1699
|
this.getPortFn = deps.getPort ?? getAvailablePort;
|
|
1523
1700
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1701
|
+
this.platform = deps.platform ?? process.platform;
|
|
1524
1702
|
this.spawnFn = deps.spawn ?? spawn;
|
|
1525
1703
|
}
|
|
1526
1704
|
async run(prompt, cwd, options) {
|
|
@@ -1564,8 +1742,8 @@ var RovoDevAgent = class {
|
|
|
1564
1742
|
}
|
|
1565
1743
|
if (this.server && !this.server.closed) await this.shutdownServer();
|
|
1566
1744
|
const port = await this.getPortFn();
|
|
1567
|
-
const detached =
|
|
1568
|
-
const child = this.spawnFn(
|
|
1745
|
+
const detached = this.platform !== "win32";
|
|
1746
|
+
const child = this.spawnFn(this.bin, [
|
|
1569
1747
|
"rovodev",
|
|
1570
1748
|
"serve",
|
|
1571
1749
|
"--disable-session-token",
|
|
@@ -1573,6 +1751,7 @@ var RovoDevAgent = class {
|
|
|
1573
1751
|
], {
|
|
1574
1752
|
cwd,
|
|
1575
1753
|
detached,
|
|
1754
|
+
shell: shouldUseWindowsShell(this.bin, this.platform),
|
|
1576
1755
|
stdio: [
|
|
1577
1756
|
"ignore",
|
|
1578
1757
|
"pipe",
|
|
@@ -1837,11 +2016,29 @@ var RovoDevAgent = class {
|
|
|
1837
2016
|
cwd: server.cwd,
|
|
1838
2017
|
port: server.port
|
|
1839
2018
|
});
|
|
1840
|
-
this.closingPromise =
|
|
2019
|
+
this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
|
|
2020
|
+
const handleClose = () => {
|
|
2021
|
+
server.child.off("close", handleClose);
|
|
2022
|
+
resolve();
|
|
2023
|
+
};
|
|
2024
|
+
server.child.on("close", handleClose);
|
|
2025
|
+
try {
|
|
2026
|
+
terminateRovoDevProcess(server.child, this.platform);
|
|
2027
|
+
} catch {
|
|
2028
|
+
server.child.off("close", handleClose);
|
|
2029
|
+
resolve();
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
setTimeout(() => {
|
|
2033
|
+
server.child.off("close", handleClose);
|
|
2034
|
+
resolve();
|
|
2035
|
+
}, 100).unref?.();
|
|
2036
|
+
}) : shutdownChildProcess(server.child, {
|
|
1841
2037
|
detached: server.detached,
|
|
1842
2038
|
killProcess: this.killProcessFn,
|
|
1843
2039
|
timeoutMs: 3e3
|
|
1844
|
-
})
|
|
2040
|
+
});
|
|
2041
|
+
this.closingPromise = this.closingPromise.finally(() => {
|
|
1845
2042
|
if (this.server === server) this.server = null;
|
|
1846
2043
|
this.closingPromise = null;
|
|
1847
2044
|
});
|
|
@@ -1875,12 +2072,12 @@ function withTimeoutSignal(signal, timeoutMs) {
|
|
|
1875
2072
|
}
|
|
1876
2073
|
//#endregion
|
|
1877
2074
|
//#region src/core/agents/factory.ts
|
|
1878
|
-
function createAgent(name, runInfo) {
|
|
2075
|
+
function createAgent(name, runInfo, pathOverride) {
|
|
1879
2076
|
switch (name) {
|
|
1880
|
-
case "claude": return new ClaudeAgent();
|
|
1881
|
-
case "codex": return new CodexAgent(runInfo.schemaPath);
|
|
1882
|
-
case "opencode": return new OpenCodeAgent();
|
|
1883
|
-
case "rovodev": return new RovoDevAgent(runInfo.schemaPath);
|
|
2077
|
+
case "claude": return new ClaudeAgent(pathOverride);
|
|
2078
|
+
case "codex": return new CodexAgent(runInfo.schemaPath, pathOverride);
|
|
2079
|
+
case "opencode": return new OpenCodeAgent({ bin: pathOverride });
|
|
2080
|
+
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, { bin: pathOverride });
|
|
1884
2081
|
}
|
|
1885
2082
|
}
|
|
1886
2083
|
//#endregion
|
|
@@ -2356,7 +2553,68 @@ function formatTokens(count) {
|
|
|
2356
2553
|
return String(count);
|
|
2357
2554
|
}
|
|
2358
2555
|
//#endregion
|
|
2556
|
+
//#region src/utils/terminal-width.ts
|
|
2557
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2558
|
+
const MARK_REGEX = /\p{Mark}/u;
|
|
2559
|
+
const REGIONAL_INDICATOR_REGEX = /\p{Regional_Indicator}/u;
|
|
2560
|
+
const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
|
|
2561
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2562
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 12871 && codePoint !== 12351 || codePoint >= 12880 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 42182 || codePoint >= 43360 && codePoint <= 43388 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65131 || codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 110592 && codePoint <= 110593 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
|
|
2563
|
+
}
|
|
2564
|
+
function codePointWidth(codePoint) {
|
|
2565
|
+
if (codePoint === 0 || codePoint === 8204 || codePoint === 8205 || codePoint === 65038 || codePoint === 65039) return 0;
|
|
2566
|
+
if (MARK_REGEX.test(String.fromCodePoint(codePoint))) return 0;
|
|
2567
|
+
return isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
2568
|
+
}
|
|
2569
|
+
function isWideEmojiGrapheme(grapheme) {
|
|
2570
|
+
return grapheme.includes("") || grapheme.includes("️") || grapheme.includes("⃣") || REGIONAL_INDICATOR_REGEX.test(grapheme) || Array.from(grapheme).some((char) => EXTENDED_PICTOGRAPHIC_REGEX.test(char));
|
|
2571
|
+
}
|
|
2572
|
+
function splitGraphemes(text) {
|
|
2573
|
+
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
2574
|
+
}
|
|
2575
|
+
function graphemeWidth(grapheme) {
|
|
2576
|
+
if (!grapheme) return 0;
|
|
2577
|
+
if (isWideEmojiGrapheme(grapheme)) return 2;
|
|
2578
|
+
let width = 0;
|
|
2579
|
+
for (const char of grapheme) width += codePointWidth(char.codePointAt(0) ?? 0);
|
|
2580
|
+
return width;
|
|
2581
|
+
}
|
|
2582
|
+
function stringWidth(text) {
|
|
2583
|
+
let width = 0;
|
|
2584
|
+
for (const grapheme of splitGraphemes(text)) width += graphemeWidth(grapheme);
|
|
2585
|
+
return width;
|
|
2586
|
+
}
|
|
2587
|
+
//#endregion
|
|
2359
2588
|
//#region src/utils/wordwrap.ts
|
|
2589
|
+
function sliceToWidth(text, width) {
|
|
2590
|
+
let result = "";
|
|
2591
|
+
let currentWidth = 0;
|
|
2592
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2593
|
+
const nextWidth = currentWidth + graphemeWidth(grapheme);
|
|
2594
|
+
if (nextWidth > width) break;
|
|
2595
|
+
result += grapheme;
|
|
2596
|
+
currentWidth = nextWidth;
|
|
2597
|
+
}
|
|
2598
|
+
return result;
|
|
2599
|
+
}
|
|
2600
|
+
function splitByWidth(text, width) {
|
|
2601
|
+
const lines = [];
|
|
2602
|
+
let current = "";
|
|
2603
|
+
let currentWidth = 0;
|
|
2604
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2605
|
+
const glyphWidth = graphemeWidth(grapheme);
|
|
2606
|
+
if (current && currentWidth + glyphWidth > width) {
|
|
2607
|
+
lines.push(current);
|
|
2608
|
+
current = grapheme;
|
|
2609
|
+
currentWidth = glyphWidth;
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
current += grapheme;
|
|
2613
|
+
currentWidth += glyphWidth;
|
|
2614
|
+
}
|
|
2615
|
+
if (current) lines.push(current);
|
|
2616
|
+
return lines;
|
|
2617
|
+
}
|
|
2360
2618
|
function wordWrap(text, width, maxLines) {
|
|
2361
2619
|
if (!text) return [];
|
|
2362
2620
|
const lines = [];
|
|
@@ -2367,26 +2625,34 @@ function wordWrap(text, width, maxLines) {
|
|
|
2367
2625
|
continue;
|
|
2368
2626
|
}
|
|
2369
2627
|
let current = "";
|
|
2628
|
+
let currentWidth = 0;
|
|
2370
2629
|
for (const word of words) {
|
|
2371
|
-
|
|
2630
|
+
const wordWidth = stringWidth(word);
|
|
2631
|
+
if (wordWidth > width) {
|
|
2372
2632
|
if (current) {
|
|
2373
2633
|
lines.push(current);
|
|
2374
2634
|
current = "";
|
|
2635
|
+
currentWidth = 0;
|
|
2375
2636
|
}
|
|
2376
|
-
for (
|
|
2637
|
+
for (const slice of splitByWidth(word, width)) lines.push(slice);
|
|
2377
2638
|
continue;
|
|
2378
2639
|
}
|
|
2379
|
-
|
|
2640
|
+
const nextWidth = current ? currentWidth + 1 + wordWidth : wordWidth;
|
|
2641
|
+
if (current && nextWidth > width) {
|
|
2380
2642
|
lines.push(current);
|
|
2381
2643
|
current = word;
|
|
2382
|
-
|
|
2644
|
+
currentWidth = wordWidth;
|
|
2645
|
+
} else {
|
|
2646
|
+
current = current ? current + " " + word : word;
|
|
2647
|
+
currentWidth = nextWidth;
|
|
2648
|
+
}
|
|
2383
2649
|
}
|
|
2384
2650
|
if (current) lines.push(current);
|
|
2385
2651
|
}
|
|
2386
2652
|
if (maxLines && lines.length > maxLines) {
|
|
2387
2653
|
const capped = lines.slice(0, maxLines);
|
|
2388
2654
|
const last = capped[maxLines - 1];
|
|
2389
|
-
capped[maxLines - 1] = last
|
|
2655
|
+
capped[maxLines - 1] = stringWidth(last) >= width ? sliceToWidth(last, width - 1) + "…" : last + "…";
|
|
2390
2656
|
return capped;
|
|
2391
2657
|
}
|
|
2392
2658
|
return lines;
|
|
@@ -2402,13 +2668,13 @@ function makeCell(char, style) {
|
|
|
2402
2668
|
return {
|
|
2403
2669
|
char,
|
|
2404
2670
|
style,
|
|
2405
|
-
width: (char
|
|
2671
|
+
width: graphemeWidth(char)
|
|
2406
2672
|
};
|
|
2407
2673
|
}
|
|
2408
2674
|
function textToCells(text, style) {
|
|
2409
2675
|
const cells = [];
|
|
2410
|
-
for (const
|
|
2411
|
-
const cell = makeCell(
|
|
2676
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2677
|
+
const cell = makeCell(grapheme, style);
|
|
2412
2678
|
cells.push(cell);
|
|
2413
2679
|
if (cell.width === 2) cells.push({
|
|
2414
2680
|
char: "",
|
|
@@ -2490,7 +2756,7 @@ const TICK_MS = 200;
|
|
|
2490
2756
|
const MOONS_PER_ROW = 30;
|
|
2491
2757
|
const MOON_PHASE_PERIOD = 1600;
|
|
2492
2758
|
const MAX_MSG_LINES = 3;
|
|
2493
|
-
const MAX_MSG_LINE_LEN =
|
|
2759
|
+
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2494
2760
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
2495
2761
|
function spacedLabel(text) {
|
|
2496
2762
|
return text.split("").join(" ");
|
|
@@ -2584,52 +2850,124 @@ function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
|
2584
2850
|
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
2585
2851
|
return cells;
|
|
2586
2852
|
}
|
|
2853
|
+
function clampCellsToWidth(content, width) {
|
|
2854
|
+
if (content.length <= width) return content;
|
|
2855
|
+
const clamped = [];
|
|
2856
|
+
let remaining = width;
|
|
2857
|
+
for (let i = 0; i < content.length && remaining > 0; i++) {
|
|
2858
|
+
const cell = content[i];
|
|
2859
|
+
if (cell.width === 0) continue;
|
|
2860
|
+
if (cell.width > remaining) break;
|
|
2861
|
+
clamped.push(cell);
|
|
2862
|
+
remaining -= cell.width;
|
|
2863
|
+
if (cell.width === 2 && content[i + 1]?.width === 0) {
|
|
2864
|
+
clamped.push(content[i + 1]);
|
|
2865
|
+
i += 1;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return clamped;
|
|
2869
|
+
}
|
|
2587
2870
|
function centerLineCells(content, width) {
|
|
2588
|
-
const
|
|
2871
|
+
const clamped = clampCellsToWidth(content, width);
|
|
2872
|
+
const w = clamped.length;
|
|
2589
2873
|
const pad = Math.max(0, Math.floor((width - w) / 2));
|
|
2590
2874
|
const rightPad = Math.max(0, width - w - pad);
|
|
2591
2875
|
return [
|
|
2592
2876
|
...emptyCells(pad),
|
|
2593
|
-
...
|
|
2877
|
+
...clamped,
|
|
2594
2878
|
...emptyCells(rightPad)
|
|
2595
2879
|
];
|
|
2596
2880
|
}
|
|
2597
2881
|
function renderResumeHintCells(width) {
|
|
2598
2882
|
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
2599
2883
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
2609
|
-
}
|
|
2610
|
-
function buildContentCells(prompt, agentName, state, elapsed, now) {
|
|
2611
|
-
const rows = [];
|
|
2884
|
+
/**
|
|
2885
|
+
* Builds the centered content viewport for the renderer.
|
|
2886
|
+
*
|
|
2887
|
+
* When `availableHeight` is constrained, the layout drops optional sections in
|
|
2888
|
+
* priority order (ASCII art, eyebrow, agent message, then prompt) so the stats
|
|
2889
|
+
* row remains visible and any remaining space is used for the newest moon rows.
|
|
2890
|
+
*/
|
|
2891
|
+
function buildContentCells(prompt, agentName, state, elapsed, now, availableHeight) {
|
|
2612
2892
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2893
|
+
const moonRows = renderMoonStripCells(state.iterations, isRunning, now);
|
|
2894
|
+
const maxRows = availableHeight ?? Infinity;
|
|
2895
|
+
if (maxRows <= 0) return [];
|
|
2896
|
+
const titleCells = renderTitleCells(agentName);
|
|
2897
|
+
const titleSpacer = titleCells[1] ?? [];
|
|
2616
2898
|
const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
|
|
2899
|
+
const promptRows = [];
|
|
2617
2900
|
for (let i = 0; i < MAX_PROMPT_LINES; i++) {
|
|
2618
2901
|
const pl = promptLines[i] ?? "";
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2902
|
+
promptRows.push(pl ? textToCells(pl, "dim") : []);
|
|
2903
|
+
}
|
|
2904
|
+
const sections = {
|
|
2905
|
+
top: [[]],
|
|
2906
|
+
eyebrow: [
|
|
2907
|
+
titleCells[0],
|
|
2908
|
+
[],
|
|
2909
|
+
[]
|
|
2910
|
+
],
|
|
2911
|
+
art: titleCells.slice(2),
|
|
2912
|
+
prompt: [
|
|
2913
|
+
titleSpacer,
|
|
2914
|
+
...promptRows,
|
|
2915
|
+
[],
|
|
2916
|
+
[]
|
|
2917
|
+
],
|
|
2918
|
+
stats: [renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount)],
|
|
2919
|
+
agent: [
|
|
2920
|
+
[],
|
|
2921
|
+
[],
|
|
2922
|
+
...renderAgentMessageCells(state.lastMessage, state.status)
|
|
2923
|
+
],
|
|
2924
|
+
moon: [
|
|
2925
|
+
[],
|
|
2926
|
+
[],
|
|
2927
|
+
...moonRows
|
|
2928
|
+
]
|
|
2929
|
+
};
|
|
2930
|
+
const flattenSections = () => [
|
|
2931
|
+
...sections.top,
|
|
2932
|
+
...sections.eyebrow,
|
|
2933
|
+
...sections.art,
|
|
2934
|
+
...sections.prompt,
|
|
2935
|
+
...sections.stats,
|
|
2936
|
+
...sections.agent,
|
|
2937
|
+
...sections.moon
|
|
2938
|
+
];
|
|
2939
|
+
const optionalSections = [
|
|
2940
|
+
"art",
|
|
2941
|
+
"eyebrow",
|
|
2942
|
+
"agent",
|
|
2943
|
+
"prompt"
|
|
2944
|
+
];
|
|
2945
|
+
let rows = flattenSections();
|
|
2946
|
+
for (const section of optionalSections) {
|
|
2947
|
+
if (rows.length <= maxRows) break;
|
|
2948
|
+
sections[section] = [];
|
|
2949
|
+
rows = flattenSections();
|
|
2950
|
+
}
|
|
2951
|
+
if (rows.length > maxRows) rows = rows.filter((row) => row.length > 0);
|
|
2952
|
+
if (rows.length > maxRows) {
|
|
2953
|
+
const nonMoonRows = [
|
|
2954
|
+
...sections.top,
|
|
2955
|
+
...sections.eyebrow,
|
|
2956
|
+
...sections.art,
|
|
2957
|
+
...sections.prompt,
|
|
2958
|
+
...sections.stats,
|
|
2959
|
+
...sections.agent
|
|
2960
|
+
].filter((row) => row.length > 0);
|
|
2961
|
+
const allowedMoonRows = Math.max(0, maxRows - nonMoonRows.length);
|
|
2962
|
+
const visibleMoonRows = allowedMoonRows === 0 ? [] : moonRows.filter((row) => row.length > 0).slice(-allowedMoonRows);
|
|
2963
|
+
rows = [...nonMoonRows, ...visibleMoonRows];
|
|
2964
|
+
}
|
|
2627
2965
|
return rows;
|
|
2628
2966
|
}
|
|
2629
2967
|
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
2630
2968
|
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
2631
2969
|
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
2632
|
-
const contentRows =
|
|
2970
|
+
const contentRows = buildContentCells(prompt, agentName, state, elapsed, now, availableHeight);
|
|
2633
2971
|
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
2634
2972
|
const contentCount = contentRows.length;
|
|
2635
2973
|
const remaining = Math.max(0, availableHeight - contentCount);
|
|
@@ -2668,11 +3006,17 @@ var Renderer = class {
|
|
|
2668
3006
|
cachedHeight = 0;
|
|
2669
3007
|
prevCells = [];
|
|
2670
3008
|
isFirstFrame = true;
|
|
3009
|
+
seedTop;
|
|
3010
|
+
seedBottom;
|
|
3011
|
+
seedSide;
|
|
2671
3012
|
constructor(orchestrator, prompt, agentName) {
|
|
2672
3013
|
this.orchestrator = orchestrator;
|
|
2673
3014
|
this.prompt = prompt;
|
|
2674
3015
|
this.agentName = agentName;
|
|
2675
3016
|
this.state = orchestrator.getState();
|
|
3017
|
+
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
3018
|
+
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
3019
|
+
this.seedSide = Math.floor(Math.random() * 2147483646) + 1;
|
|
2676
3020
|
this.exitPromise = new Promise((resolve) => {
|
|
2677
3021
|
this.exitResolve = resolve;
|
|
2678
3022
|
});
|
|
@@ -2736,9 +3080,9 @@ var Renderer = class {
|
|
|
2736
3080
|
rest: "dim"
|
|
2737
3081
|
} : star;
|
|
2738
3082
|
};
|
|
2739
|
-
this.topStars = generateStarField(w, h, STAR_DENSITY,
|
|
2740
|
-
this.bottomStars = generateStarField(w, h, STAR_DENSITY,
|
|
2741
|
-
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY,
|
|
3083
|
+
this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
3084
|
+
this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
3085
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
|
|
2742
3086
|
return true;
|
|
2743
3087
|
}
|
|
2744
3088
|
return false;
|
|
@@ -2934,7 +3278,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
2934
3278
|
}
|
|
2935
3279
|
}
|
|
2936
3280
|
appendDebugLog("run:start", { args: process$1.argv.slice(2) });
|
|
2937
|
-
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration, {
|
|
3281
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
|
|
2938
3282
|
maxIterations: options.maxIterations,
|
|
2939
3283
|
maxTokens: options.maxTokens
|
|
2940
3284
|
});
|