gnhf 0.1.6 → 0.1.7
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 +16 -8
- package/dist/cli.mjs +1072 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ You wake up to a branch full of clean work and a log of everything that happened
|
|
|
44
44
|
|
|
45
45
|
- **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
|
|
46
46
|
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries and exponential backoff
|
|
47
|
-
- **Agent-agnostic** — works with Claude Code or
|
|
47
|
+
- **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
|
|
48
48
|
|
|
49
49
|
## Quick Start
|
|
50
50
|
|
|
@@ -80,6 +80,10 @@ npm run build
|
|
|
80
80
|
npm link
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
If you want to run `gnhf --agent rovodev`, install Atlassian's `acli` and authenticate it with Rovo Dev first.
|
|
84
|
+
|
|
85
|
+
If you want to run `gnhf --agent opencode`, install `opencode` and authenticate at least one provider first.
|
|
86
|
+
|
|
83
87
|
## How It Works
|
|
84
88
|
|
|
85
89
|
```
|
|
@@ -141,19 +145,19 @@ npm link
|
|
|
141
145
|
|
|
142
146
|
### Flags
|
|
143
147
|
|
|
144
|
-
| Flag | Description
|
|
145
|
-
| ---------------------- |
|
|
146
|
-
| `--agent <agent>` | Agent to use (`claude` or `
|
|
147
|
-
| `--max-iterations <n>` | Abort after `n` total iterations
|
|
148
|
-
| `--max-tokens <n>` | Abort after `n` total input+output tokens
|
|
149
|
-
| `--version` | Show version
|
|
148
|
+
| Flag | Description | Default |
|
|
149
|
+
| ---------------------- | ---------------------------------------------------------- | ---------------------- |
|
|
150
|
+
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
|
|
151
|
+
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
152
|
+
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
153
|
+
| `--version` | Show version | |
|
|
150
154
|
|
|
151
155
|
## Configuration
|
|
152
156
|
|
|
153
157
|
Config lives at `~/.gnhf/config.yml`:
|
|
154
158
|
|
|
155
159
|
```yaml
|
|
156
|
-
# Agent to use by default
|
|
160
|
+
# Agent to use by default (claude, codex, rovodev, or opencode)
|
|
157
161
|
agent: claude
|
|
158
162
|
|
|
159
163
|
# Abort after this many consecutive failures
|
|
@@ -164,6 +168,10 @@ If the file does not exist yet, `gnhf` creates it on first run using the resolve
|
|
|
164
168
|
|
|
165
169
|
CLI flags override config file values. The iteration and token caps are runtime-only flags and are not persisted in `config.yml`.
|
|
166
170
|
|
|
171
|
+
When using `agent: rovodev`, `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. That requires `acli` to be installed and already authenticated for Rovo Dev.
|
|
172
|
+
|
|
173
|
+
When using `agent: opencode`, `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session for the target workspace, and applies a blanket `{"permission":"*","pattern":"*","action":"allow"}` rule so tool calls do not block on prompts. That requires the `opencode` CLI to be installed and already configured with a usable model provider.
|
|
174
|
+
|
|
167
175
|
## Development
|
|
168
176
|
|
|
169
177
|
```sh
|
package/dist/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { dirname, isAbsolute, join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
10
|
+
import { createServer } from "node:net";
|
|
10
11
|
import { EventEmitter } from "node:events";
|
|
11
12
|
import { createHash } from "node:crypto";
|
|
12
13
|
//#region src/core/config.ts
|
|
@@ -453,11 +454,984 @@ var CodexAgent = class {
|
|
|
453
454
|
}
|
|
454
455
|
};
|
|
455
456
|
//#endregion
|
|
457
|
+
//#region src/core/agents/opencode.ts
|
|
458
|
+
const BLANKET_PERMISSION_RULESET = [{
|
|
459
|
+
permission: "*",
|
|
460
|
+
pattern: "*",
|
|
461
|
+
action: "allow"
|
|
462
|
+
}];
|
|
463
|
+
const STRUCTURED_OUTPUT_FORMAT = {
|
|
464
|
+
type: "json_schema",
|
|
465
|
+
schema: AGENT_OUTPUT_SCHEMA,
|
|
466
|
+
retryCount: 1
|
|
467
|
+
};
|
|
468
|
+
function buildOpencodeChildEnv() {
|
|
469
|
+
const env = { ...process.env };
|
|
470
|
+
delete env.OPENCODE_SERVER_USERNAME;
|
|
471
|
+
delete env.OPENCODE_SERVER_PASSWORD;
|
|
472
|
+
return env;
|
|
473
|
+
}
|
|
474
|
+
function buildPrompt(prompt) {
|
|
475
|
+
return [
|
|
476
|
+
prompt,
|
|
477
|
+
"",
|
|
478
|
+
"When you finish, reply with only valid JSON.",
|
|
479
|
+
"Do not wrap the JSON in markdown fences.",
|
|
480
|
+
"Do not include any prose before or after the JSON.",
|
|
481
|
+
`The JSON must match this schema exactly: ${JSON.stringify(AGENT_OUTPUT_SCHEMA)}`
|
|
482
|
+
].join("\n");
|
|
483
|
+
}
|
|
484
|
+
function createAbortError$1() {
|
|
485
|
+
return /* @__PURE__ */ new Error("Agent was aborted");
|
|
486
|
+
}
|
|
487
|
+
function isAgentAbortError(error) {
|
|
488
|
+
return error instanceof Error && error.message === "Agent was aborted";
|
|
489
|
+
}
|
|
490
|
+
function isAbortError$1(error) {
|
|
491
|
+
return error instanceof Error && error.name === "AbortError";
|
|
492
|
+
}
|
|
493
|
+
function getAvailablePort$1() {
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
const server = createServer();
|
|
496
|
+
server.unref();
|
|
497
|
+
server.on("error", reject);
|
|
498
|
+
server.listen(0, "127.0.0.1", () => {
|
|
499
|
+
const address = server.address();
|
|
500
|
+
if (!address || typeof address === "string") {
|
|
501
|
+
server.close();
|
|
502
|
+
reject(/* @__PURE__ */ new Error("Failed to allocate a port for opencode"));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
server.close((error) => {
|
|
506
|
+
if (error) {
|
|
507
|
+
reject(error);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
resolve(address.port);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function delay$1(ms, signal) {
|
|
516
|
+
if (!signal) {
|
|
517
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
await new Promise((resolve, reject) => {
|
|
521
|
+
const timer = setTimeout(() => {
|
|
522
|
+
signal.removeEventListener("abort", onAbort);
|
|
523
|
+
resolve();
|
|
524
|
+
}, ms);
|
|
525
|
+
const onAbort = () => {
|
|
526
|
+
clearTimeout(timer);
|
|
527
|
+
signal.removeEventListener("abort", onAbort);
|
|
528
|
+
reject(createAbortError$1());
|
|
529
|
+
};
|
|
530
|
+
if (signal.aborted) {
|
|
531
|
+
onAbort();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function toUsage(tokens) {
|
|
538
|
+
return {
|
|
539
|
+
inputTokens: tokens?.input ?? 0,
|
|
540
|
+
outputTokens: tokens?.output ?? 0,
|
|
541
|
+
cacheReadTokens: tokens?.cache?.read ?? 0,
|
|
542
|
+
cacheCreationTokens: tokens?.cache?.write ?? 0
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function withTimeoutSignal$1(signal, timeoutMs) {
|
|
546
|
+
if (timeoutMs === void 0) return signal;
|
|
547
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
548
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
549
|
+
}
|
|
550
|
+
var OpenCodeAgent = class {
|
|
551
|
+
name = "opencode";
|
|
552
|
+
fetchFn;
|
|
553
|
+
getPortFn;
|
|
554
|
+
killProcessFn;
|
|
555
|
+
spawnFn;
|
|
556
|
+
server = null;
|
|
557
|
+
closingPromise = null;
|
|
558
|
+
constructor(deps = {}) {
|
|
559
|
+
this.fetchFn = deps.fetch ?? fetch;
|
|
560
|
+
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
561
|
+
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
562
|
+
this.spawnFn = deps.spawn ?? spawn;
|
|
563
|
+
}
|
|
564
|
+
async run(prompt, cwd, options) {
|
|
565
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
566
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
567
|
+
const runController = new AbortController();
|
|
568
|
+
let sessionId = null;
|
|
569
|
+
const onAbort = () => {
|
|
570
|
+
runController.abort();
|
|
571
|
+
};
|
|
572
|
+
if (signal?.aborted) {
|
|
573
|
+
logStream?.end();
|
|
574
|
+
throw createAbortError$1();
|
|
575
|
+
}
|
|
576
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
577
|
+
try {
|
|
578
|
+
const server = await this.ensureServer(cwd, runController.signal);
|
|
579
|
+
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
580
|
+
return await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (runController.signal.aborted || isAbortError$1(error)) throw createAbortError$1();
|
|
583
|
+
throw error;
|
|
584
|
+
} finally {
|
|
585
|
+
signal?.removeEventListener("abort", onAbort);
|
|
586
|
+
logStream?.end();
|
|
587
|
+
if (this.server && sessionId) {
|
|
588
|
+
if (runController.signal.aborted) await this.abortSession(this.server, sessionId);
|
|
589
|
+
await this.deleteSession(this.server, sessionId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async close() {
|
|
594
|
+
await this.shutdownServer();
|
|
595
|
+
}
|
|
596
|
+
async ensureServer(cwd, signal) {
|
|
597
|
+
if (this.server && !this.server.closed) if (this.server.cwd !== cwd) await this.shutdownServer();
|
|
598
|
+
else {
|
|
599
|
+
await this.server.readyPromise;
|
|
600
|
+
return this.server;
|
|
601
|
+
}
|
|
602
|
+
if (this.server && !this.server.closed) {
|
|
603
|
+
await this.server.readyPromise;
|
|
604
|
+
return this.server;
|
|
605
|
+
}
|
|
606
|
+
const port = await this.getPortFn();
|
|
607
|
+
const detached = process.platform !== "win32";
|
|
608
|
+
const child = this.spawnFn("opencode", [
|
|
609
|
+
"serve",
|
|
610
|
+
"--hostname",
|
|
611
|
+
"127.0.0.1",
|
|
612
|
+
"--port",
|
|
613
|
+
String(port),
|
|
614
|
+
"--print-logs"
|
|
615
|
+
], {
|
|
616
|
+
cwd,
|
|
617
|
+
detached,
|
|
618
|
+
stdio: [
|
|
619
|
+
"ignore",
|
|
620
|
+
"pipe",
|
|
621
|
+
"pipe"
|
|
622
|
+
],
|
|
623
|
+
env: buildOpencodeChildEnv()
|
|
624
|
+
});
|
|
625
|
+
const server = {
|
|
626
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
627
|
+
child,
|
|
628
|
+
closed: false,
|
|
629
|
+
cwd,
|
|
630
|
+
detached,
|
|
631
|
+
port,
|
|
632
|
+
readyPromise: Promise.resolve(),
|
|
633
|
+
stderr: "",
|
|
634
|
+
stdout: ""
|
|
635
|
+
};
|
|
636
|
+
const maxOutput = 64 * 1024;
|
|
637
|
+
child.stdout.on("data", (data) => {
|
|
638
|
+
server.stdout += data.toString();
|
|
639
|
+
if (server.stdout.length > maxOutput) server.stdout = server.stdout.slice(-maxOutput);
|
|
640
|
+
});
|
|
641
|
+
child.stderr.on("data", (data) => {
|
|
642
|
+
server.stderr += data.toString();
|
|
643
|
+
if (server.stderr.length > maxOutput) server.stderr = server.stderr.slice(-maxOutput);
|
|
644
|
+
});
|
|
645
|
+
child.on("close", () => {
|
|
646
|
+
server.closed = true;
|
|
647
|
+
if (this.server === server) this.server = null;
|
|
648
|
+
});
|
|
649
|
+
this.server = server;
|
|
650
|
+
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
651
|
+
await this.shutdownServer();
|
|
652
|
+
throw error;
|
|
653
|
+
});
|
|
654
|
+
await server.readyPromise;
|
|
655
|
+
return server;
|
|
656
|
+
}
|
|
657
|
+
async waitForHealthy(server, signal) {
|
|
658
|
+
const deadline = Date.now() + 3e4;
|
|
659
|
+
let spawnErrorMessage = null;
|
|
660
|
+
server.child.once("error", (error) => {
|
|
661
|
+
spawnErrorMessage = error.message;
|
|
662
|
+
});
|
|
663
|
+
while (Date.now() < deadline) {
|
|
664
|
+
if (signal?.aborted) throw createAbortError$1();
|
|
665
|
+
if (spawnErrorMessage) throw new Error(`Failed to spawn opencode: ${spawnErrorMessage}`);
|
|
666
|
+
if (server.closed) {
|
|
667
|
+
const output = server.stderr.trim() || server.stdout.trim();
|
|
668
|
+
throw new Error(output ? `opencode exited before becoming ready: ${output}` : "opencode exited before becoming ready");
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
if ((await this.fetchFn(`${server.baseUrl}/global/health`, {
|
|
672
|
+
method: "GET",
|
|
673
|
+
signal
|
|
674
|
+
})).ok) return;
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (isAbortError$1(error)) throw createAbortError$1();
|
|
677
|
+
}
|
|
678
|
+
await delay$1(250, signal);
|
|
679
|
+
}
|
|
680
|
+
throw new Error(`Timed out waiting for opencode serve to become ready on port ${server.port}`);
|
|
681
|
+
}
|
|
682
|
+
async createSession(server, cwd, signal) {
|
|
683
|
+
return (await this.requestJSON(server, "/session", {
|
|
684
|
+
method: "POST",
|
|
685
|
+
body: {
|
|
686
|
+
directory: cwd,
|
|
687
|
+
permission: BLANKET_PERMISSION_RULESET
|
|
688
|
+
},
|
|
689
|
+
signal
|
|
690
|
+
})).id;
|
|
691
|
+
}
|
|
692
|
+
async streamMessage(server, sessionId, prompt, signal, logStream, onUsage, onMessage) {
|
|
693
|
+
const streamAbortController = new AbortController();
|
|
694
|
+
const streamSignal = AbortSignal.any([signal, streamAbortController.signal]);
|
|
695
|
+
const eventResponse = await this.request(server, "/global/event", {
|
|
696
|
+
method: "GET",
|
|
697
|
+
headers: { accept: "text/event-stream" },
|
|
698
|
+
signal: streamSignal
|
|
699
|
+
});
|
|
700
|
+
if (!eventResponse.body) throw new Error("opencode returned no event stream body");
|
|
701
|
+
let messageRequestError = null;
|
|
702
|
+
const messageRequest = (async () => {
|
|
703
|
+
try {
|
|
704
|
+
return {
|
|
705
|
+
ok: true,
|
|
706
|
+
body: await this.requestText(server, `/session/${sessionId}/message`, {
|
|
707
|
+
method: "POST",
|
|
708
|
+
body: {
|
|
709
|
+
role: "user",
|
|
710
|
+
parts: [{
|
|
711
|
+
type: "text",
|
|
712
|
+
text: prompt
|
|
713
|
+
}],
|
|
714
|
+
format: STRUCTURED_OUTPUT_FORMAT
|
|
715
|
+
},
|
|
716
|
+
signal
|
|
717
|
+
})
|
|
718
|
+
};
|
|
719
|
+
} catch (error) {
|
|
720
|
+
messageRequestError = error;
|
|
721
|
+
streamAbortController.abort();
|
|
722
|
+
return {
|
|
723
|
+
ok: false,
|
|
724
|
+
error
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
})();
|
|
728
|
+
const usage = {
|
|
729
|
+
inputTokens: 0,
|
|
730
|
+
outputTokens: 0,
|
|
731
|
+
cacheReadTokens: 0,
|
|
732
|
+
cacheCreationTokens: 0
|
|
733
|
+
};
|
|
734
|
+
const usageByMessageId = /* @__PURE__ */ new Map();
|
|
735
|
+
const textParts = /* @__PURE__ */ new Map();
|
|
736
|
+
let lastText = null;
|
|
737
|
+
let lastFinalAnswerText = null;
|
|
738
|
+
let lastUsageSignature = "0:0:0:0";
|
|
739
|
+
const updateUsage = (messageId, tokens) => {
|
|
740
|
+
if (!messageId || !tokens) return;
|
|
741
|
+
usageByMessageId.set(messageId, toUsage(tokens));
|
|
742
|
+
let nextInputTokens = 0;
|
|
743
|
+
let nextOutputTokens = 0;
|
|
744
|
+
let nextCacheReadTokens = 0;
|
|
745
|
+
let nextCacheCreationTokens = 0;
|
|
746
|
+
for (const messageUsage of usageByMessageId.values()) {
|
|
747
|
+
nextInputTokens += messageUsage.inputTokens;
|
|
748
|
+
nextOutputTokens += messageUsage.outputTokens;
|
|
749
|
+
nextCacheReadTokens += messageUsage.cacheReadTokens;
|
|
750
|
+
nextCacheCreationTokens += messageUsage.cacheCreationTokens;
|
|
751
|
+
}
|
|
752
|
+
const signature = [
|
|
753
|
+
nextInputTokens,
|
|
754
|
+
nextOutputTokens,
|
|
755
|
+
nextCacheReadTokens,
|
|
756
|
+
nextCacheCreationTokens
|
|
757
|
+
].join(":");
|
|
758
|
+
usage.inputTokens = nextInputTokens;
|
|
759
|
+
usage.outputTokens = nextOutputTokens;
|
|
760
|
+
usage.cacheReadTokens = nextCacheReadTokens;
|
|
761
|
+
usage.cacheCreationTokens = nextCacheCreationTokens;
|
|
762
|
+
if (signature !== lastUsageSignature) {
|
|
763
|
+
lastUsageSignature = signature;
|
|
764
|
+
onUsage?.({ ...usage });
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
const emitText = (partId, nextText, phase) => {
|
|
768
|
+
const trimmed = nextText.trim();
|
|
769
|
+
textParts.set(partId, {
|
|
770
|
+
text: nextText,
|
|
771
|
+
phase
|
|
772
|
+
});
|
|
773
|
+
if (!trimmed) return;
|
|
774
|
+
lastText = nextText;
|
|
775
|
+
if (phase === "final_answer") lastFinalAnswerText = nextText;
|
|
776
|
+
onMessage?.(trimmed);
|
|
777
|
+
};
|
|
778
|
+
const handleEvent = (event) => {
|
|
779
|
+
const payload = event.payload;
|
|
780
|
+
const properties = payload?.properties;
|
|
781
|
+
if (!properties || properties.sessionID !== sessionId) return false;
|
|
782
|
+
if (payload?.type === "message.part.delta" && properties.field === "text" && typeof properties.partID === "string" && typeof properties.delta === "string") {
|
|
783
|
+
const current = textParts.get(properties.partID);
|
|
784
|
+
emitText(properties.partID, `${current?.text ?? ""}${properties.delta}`, current?.phase);
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
if (payload?.type === "message.part.updated") {
|
|
788
|
+
const part = properties.part;
|
|
789
|
+
if (!part) return false;
|
|
790
|
+
if (part.type === "text" && typeof part.id === "string") {
|
|
791
|
+
emitText(part.id, part.text ?? "", part.metadata?.openai?.phase);
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
if (part.type === "step-finish") {
|
|
795
|
+
updateUsage(part.messageID, part.tokens);
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
if (payload?.type === "message.updated") {
|
|
801
|
+
if (properties.info?.role === "assistant") updateUsage(properties.info.id, properties.info.tokens);
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
return payload?.type === "session.idle";
|
|
805
|
+
};
|
|
806
|
+
const decoder = new TextDecoder();
|
|
807
|
+
const reader = eventResponse.body.getReader();
|
|
808
|
+
let buffer = "";
|
|
809
|
+
let sawSessionIdle = false;
|
|
810
|
+
const processRawEvent = (rawEvent) => {
|
|
811
|
+
if (!rawEvent.trim()) return;
|
|
812
|
+
const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
|
|
813
|
+
if (dataLines.length === 0) return;
|
|
814
|
+
try {
|
|
815
|
+
if (handleEvent(JSON.parse(dataLines.join("\n")))) sawSessionIdle = true;
|
|
816
|
+
} catch {}
|
|
817
|
+
};
|
|
818
|
+
const processBufferedEvents = (flushRemainder = false) => {
|
|
819
|
+
while (true) {
|
|
820
|
+
const lfBoundary = buffer.indexOf("\n\n");
|
|
821
|
+
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
822
|
+
let boundary;
|
|
823
|
+
let separatorLen;
|
|
824
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
825
|
+
if (crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary)) {
|
|
826
|
+
boundary = crlfBoundary;
|
|
827
|
+
separatorLen = 4;
|
|
828
|
+
} else {
|
|
829
|
+
boundary = lfBoundary;
|
|
830
|
+
separatorLen = 2;
|
|
831
|
+
}
|
|
832
|
+
processRawEvent(buffer.slice(0, boundary));
|
|
833
|
+
buffer = buffer.slice(boundary + separatorLen);
|
|
834
|
+
if (sawSessionIdle) return;
|
|
835
|
+
}
|
|
836
|
+
if (flushRemainder && buffer.trim()) {
|
|
837
|
+
processRawEvent(buffer);
|
|
838
|
+
buffer = "";
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
try {
|
|
842
|
+
while (!sawSessionIdle) {
|
|
843
|
+
let readResult;
|
|
844
|
+
try {
|
|
845
|
+
readResult = await reader.read();
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (messageRequestError) {
|
|
848
|
+
if (isAbortError$1(messageRequestError) || isAgentAbortError(messageRequestError)) throw createAbortError$1();
|
|
849
|
+
throw messageRequestError;
|
|
850
|
+
}
|
|
851
|
+
if (isAbortError$1(error)) throw createAbortError$1();
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
if (readResult.done) {
|
|
855
|
+
const tail = decoder.decode();
|
|
856
|
+
if (tail) {
|
|
857
|
+
logStream?.write(tail);
|
|
858
|
+
buffer += tail;
|
|
859
|
+
}
|
|
860
|
+
processBufferedEvents(true);
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
864
|
+
logStream?.write(chunk);
|
|
865
|
+
buffer += chunk;
|
|
866
|
+
processBufferedEvents();
|
|
867
|
+
}
|
|
868
|
+
} finally {
|
|
869
|
+
streamAbortController.abort();
|
|
870
|
+
await reader.cancel().catch(() => void 0);
|
|
871
|
+
}
|
|
872
|
+
const messageResult = await messageRequest;
|
|
873
|
+
if (!messageResult.ok) {
|
|
874
|
+
if (isAbortError$1(messageResult.error) || isAgentAbortError(messageResult.error)) throw createAbortError$1();
|
|
875
|
+
throw messageResult.error;
|
|
876
|
+
}
|
|
877
|
+
const body = messageResult.body;
|
|
878
|
+
let response;
|
|
879
|
+
try {
|
|
880
|
+
response = JSON.parse(body);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
throw new Error(`Failed to parse opencode response: ${error instanceof Error ? error.message : String(error)}`);
|
|
883
|
+
}
|
|
884
|
+
if (response.info?.role === "assistant") updateUsage(response.info.id, response.info.tokens);
|
|
885
|
+
for (const part of response.parts ?? []) {
|
|
886
|
+
if (part.type !== "text" || typeof part.text !== "string") continue;
|
|
887
|
+
if (!part.text.trim()) continue;
|
|
888
|
+
lastText = part.text;
|
|
889
|
+
if (part.metadata?.openai?.phase === "final_answer") lastFinalAnswerText = part.text;
|
|
890
|
+
}
|
|
891
|
+
if (response.info?.structured) return {
|
|
892
|
+
output: response.info.structured,
|
|
893
|
+
usage
|
|
894
|
+
};
|
|
895
|
+
const outputText = lastFinalAnswerText ?? lastText;
|
|
896
|
+
if (!outputText) throw new Error("opencode returned no text output");
|
|
897
|
+
try {
|
|
898
|
+
return {
|
|
899
|
+
output: JSON.parse(outputText),
|
|
900
|
+
usage
|
|
901
|
+
};
|
|
902
|
+
} catch (error) {
|
|
903
|
+
throw new Error(`Failed to parse opencode output: ${error instanceof Error ? error.message : String(error)}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async deleteSession(server, sessionId) {
|
|
907
|
+
try {
|
|
908
|
+
await this.request(server, `/session/${sessionId}`, {
|
|
909
|
+
method: "DELETE",
|
|
910
|
+
timeoutMs: 1e3
|
|
911
|
+
});
|
|
912
|
+
} catch {}
|
|
913
|
+
}
|
|
914
|
+
async abortSession(server, sessionId) {
|
|
915
|
+
try {
|
|
916
|
+
await this.request(server, `/session/${sessionId}/abort`, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
timeoutMs: 1e3
|
|
919
|
+
});
|
|
920
|
+
} catch {}
|
|
921
|
+
}
|
|
922
|
+
async shutdownServer() {
|
|
923
|
+
if (!this.server || this.server.closed) {
|
|
924
|
+
this.server = null;
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (this.closingPromise) {
|
|
928
|
+
await this.closingPromise;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const server = this.server;
|
|
932
|
+
const waitForClose = new Promise((resolve) => {
|
|
933
|
+
if (server.closed) {
|
|
934
|
+
resolve();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
server.child.once("close", () => resolve());
|
|
938
|
+
});
|
|
939
|
+
try {
|
|
940
|
+
this.signalServer(server, "SIGTERM");
|
|
941
|
+
} catch {}
|
|
942
|
+
const forceKill = new Promise((resolve) => {
|
|
943
|
+
setTimeout(() => {
|
|
944
|
+
if (!server.closed) try {
|
|
945
|
+
this.signalServer(server, "SIGKILL");
|
|
946
|
+
} catch {}
|
|
947
|
+
resolve();
|
|
948
|
+
}, 3e3).unref?.();
|
|
949
|
+
});
|
|
950
|
+
this.closingPromise = Promise.race([waitForClose, forceKill]).finally(() => {
|
|
951
|
+
if (this.server === server) this.server = null;
|
|
952
|
+
this.closingPromise = null;
|
|
953
|
+
});
|
|
954
|
+
await this.closingPromise;
|
|
955
|
+
}
|
|
956
|
+
signalServer(server, signal) {
|
|
957
|
+
if (server.detached && server.child.pid) try {
|
|
958
|
+
this.killProcessFn(-server.child.pid, signal);
|
|
959
|
+
return;
|
|
960
|
+
} catch {}
|
|
961
|
+
server.child.kill(signal);
|
|
962
|
+
}
|
|
963
|
+
async requestJSON(server, path, options) {
|
|
964
|
+
const body = await this.requestText(server, path, options);
|
|
965
|
+
return JSON.parse(body);
|
|
966
|
+
}
|
|
967
|
+
async requestText(server, path, options) {
|
|
968
|
+
return await (await this.request(server, path, options)).text();
|
|
969
|
+
}
|
|
970
|
+
async request(server, path, options) {
|
|
971
|
+
const headers = new Headers(options.headers);
|
|
972
|
+
if (options.body !== void 0) headers.set("content-type", "application/json");
|
|
973
|
+
const signal = withTimeoutSignal$1(options.signal, options.timeoutMs);
|
|
974
|
+
const response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
975
|
+
method: options.method,
|
|
976
|
+
headers,
|
|
977
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
978
|
+
signal
|
|
979
|
+
});
|
|
980
|
+
if (!response.ok) {
|
|
981
|
+
const body = await response.text();
|
|
982
|
+
throw new Error(`opencode ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
983
|
+
}
|
|
984
|
+
return response;
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/core/agents/rovodev.ts
|
|
989
|
+
function buildSystemPrompt(schema) {
|
|
990
|
+
return [
|
|
991
|
+
"You are the coding agent used by gnhf.",
|
|
992
|
+
"Work autonomously in the current workspace and use tools when needed.",
|
|
993
|
+
"When you finish, reply with only valid JSON.",
|
|
994
|
+
"Do not wrap the JSON in markdown fences.",
|
|
995
|
+
"Do not include any prose before or after the JSON.",
|
|
996
|
+
`The JSON must match this schema exactly: ${schema}`
|
|
997
|
+
].join(" ");
|
|
998
|
+
}
|
|
999
|
+
function createAbortError() {
|
|
1000
|
+
return /* @__PURE__ */ new Error("Agent was aborted");
|
|
1001
|
+
}
|
|
1002
|
+
function isAbortError(error) {
|
|
1003
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1004
|
+
}
|
|
1005
|
+
function getAvailablePort() {
|
|
1006
|
+
return new Promise((resolve, reject) => {
|
|
1007
|
+
const server = createServer();
|
|
1008
|
+
server.unref();
|
|
1009
|
+
server.on("error", reject);
|
|
1010
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1011
|
+
const address = server.address();
|
|
1012
|
+
if (!address || typeof address === "string") {
|
|
1013
|
+
server.close();
|
|
1014
|
+
reject(/* @__PURE__ */ new Error("Failed to allocate a port for rovodev"));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
server.close((error) => {
|
|
1018
|
+
if (error) {
|
|
1019
|
+
reject(error);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
resolve(address.port);
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
async function delay(ms, signal) {
|
|
1028
|
+
if (!signal) {
|
|
1029
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
await new Promise((resolve, reject) => {
|
|
1033
|
+
const timer = setTimeout(() => {
|
|
1034
|
+
signal.removeEventListener("abort", onAbort);
|
|
1035
|
+
resolve();
|
|
1036
|
+
}, ms);
|
|
1037
|
+
const onAbort = () => {
|
|
1038
|
+
clearTimeout(timer);
|
|
1039
|
+
signal.removeEventListener("abort", onAbort);
|
|
1040
|
+
reject(createAbortError());
|
|
1041
|
+
};
|
|
1042
|
+
if (signal.aborted) {
|
|
1043
|
+
onAbort();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
var RovoDevAgent = class {
|
|
1050
|
+
name = "rovodev";
|
|
1051
|
+
schemaPath;
|
|
1052
|
+
fetchFn;
|
|
1053
|
+
getPortFn;
|
|
1054
|
+
killProcessFn;
|
|
1055
|
+
spawnFn;
|
|
1056
|
+
server = null;
|
|
1057
|
+
closingPromise = null;
|
|
1058
|
+
constructor(schemaPath, deps = {}) {
|
|
1059
|
+
this.schemaPath = schemaPath;
|
|
1060
|
+
this.fetchFn = deps.fetch ?? fetch;
|
|
1061
|
+
this.getPortFn = deps.getPort ?? getAvailablePort;
|
|
1062
|
+
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1063
|
+
this.spawnFn = deps.spawn ?? spawn;
|
|
1064
|
+
}
|
|
1065
|
+
async run(prompt, cwd, options) {
|
|
1066
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1067
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1068
|
+
const runController = new AbortController();
|
|
1069
|
+
let sessionId = null;
|
|
1070
|
+
const onAbort = () => {
|
|
1071
|
+
runController.abort();
|
|
1072
|
+
};
|
|
1073
|
+
if (signal?.aborted) {
|
|
1074
|
+
logStream?.end();
|
|
1075
|
+
throw createAbortError();
|
|
1076
|
+
}
|
|
1077
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1078
|
+
try {
|
|
1079
|
+
const server = await this.ensureServer(cwd, runController.signal);
|
|
1080
|
+
sessionId = await this.createSession(server, runController.signal);
|
|
1081
|
+
await this.setInlineSystemPrompt(server, sessionId, runController.signal);
|
|
1082
|
+
await this.setChatMessage(server, sessionId, prompt, runController.signal);
|
|
1083
|
+
return await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (runController.signal.aborted || isAbortError(error)) throw createAbortError();
|
|
1086
|
+
throw error;
|
|
1087
|
+
} finally {
|
|
1088
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1089
|
+
logStream?.end();
|
|
1090
|
+
if (this.server && sessionId) {
|
|
1091
|
+
if (runController.signal.aborted) await this.cancelSession(this.server, sessionId);
|
|
1092
|
+
await this.deleteSession(this.server, sessionId);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async close() {
|
|
1097
|
+
await this.shutdownServer();
|
|
1098
|
+
}
|
|
1099
|
+
async ensureServer(cwd, signal) {
|
|
1100
|
+
if (this.server && !this.server.closed && this.server.cwd === cwd) {
|
|
1101
|
+
await this.server.readyPromise;
|
|
1102
|
+
return this.server;
|
|
1103
|
+
}
|
|
1104
|
+
if (this.server && !this.server.closed) await this.shutdownServer();
|
|
1105
|
+
const port = await this.getPortFn();
|
|
1106
|
+
const detached = process.platform !== "win32";
|
|
1107
|
+
const child = this.spawnFn("acli", [
|
|
1108
|
+
"rovodev",
|
|
1109
|
+
"serve",
|
|
1110
|
+
"--disable-session-token",
|
|
1111
|
+
String(port)
|
|
1112
|
+
], {
|
|
1113
|
+
cwd,
|
|
1114
|
+
detached,
|
|
1115
|
+
stdio: [
|
|
1116
|
+
"ignore",
|
|
1117
|
+
"pipe",
|
|
1118
|
+
"pipe"
|
|
1119
|
+
],
|
|
1120
|
+
env: process.env
|
|
1121
|
+
});
|
|
1122
|
+
const server = {
|
|
1123
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
1124
|
+
child,
|
|
1125
|
+
cwd,
|
|
1126
|
+
detached,
|
|
1127
|
+
port,
|
|
1128
|
+
readyPromise: Promise.resolve(),
|
|
1129
|
+
closed: false,
|
|
1130
|
+
stdout: "",
|
|
1131
|
+
stderr: ""
|
|
1132
|
+
};
|
|
1133
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
1134
|
+
child.stdout.on("data", (data) => {
|
|
1135
|
+
server.stdout += data.toString();
|
|
1136
|
+
if (server.stdout.length > MAX_OUTPUT) server.stdout = server.stdout.slice(-MAX_OUTPUT);
|
|
1137
|
+
});
|
|
1138
|
+
child.stderr.on("data", (data) => {
|
|
1139
|
+
server.stderr += data.toString();
|
|
1140
|
+
if (server.stderr.length > MAX_OUTPUT) server.stderr = server.stderr.slice(-MAX_OUTPUT);
|
|
1141
|
+
});
|
|
1142
|
+
child.on("close", () => {
|
|
1143
|
+
server.closed = true;
|
|
1144
|
+
if (this.server === server) this.server = null;
|
|
1145
|
+
});
|
|
1146
|
+
this.server = server;
|
|
1147
|
+
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
1148
|
+
await this.shutdownServer();
|
|
1149
|
+
throw error;
|
|
1150
|
+
});
|
|
1151
|
+
await server.readyPromise;
|
|
1152
|
+
return server;
|
|
1153
|
+
}
|
|
1154
|
+
async waitForHealthy(server, signal) {
|
|
1155
|
+
const deadline = Date.now() + 3e4;
|
|
1156
|
+
let spawnError = null;
|
|
1157
|
+
server.child.once("error", (error) => {
|
|
1158
|
+
spawnError = error;
|
|
1159
|
+
});
|
|
1160
|
+
while (Date.now() < deadline) {
|
|
1161
|
+
if (signal?.aborted) throw createAbortError();
|
|
1162
|
+
if (spawnError) throw new Error(`Failed to spawn rovodev: ${spawnError.message}`);
|
|
1163
|
+
if (server.closed) {
|
|
1164
|
+
const output = server.stderr.trim() || server.stdout.trim();
|
|
1165
|
+
throw new Error(output ? `rovodev exited before becoming ready: ${output}` : "rovodev exited before becoming ready");
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
if ((await this.fetchFn(`${server.baseUrl}/healthcheck`, {
|
|
1169
|
+
method: "GET",
|
|
1170
|
+
signal
|
|
1171
|
+
})).ok) return;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (isAbortError(error)) throw createAbortError();
|
|
1174
|
+
}
|
|
1175
|
+
await delay(250, signal);
|
|
1176
|
+
}
|
|
1177
|
+
throw new Error(`Timed out waiting for rovodev serve to become ready on port ${server.port}`);
|
|
1178
|
+
}
|
|
1179
|
+
async createSession(server, signal) {
|
|
1180
|
+
return (await this.requestJSON(server, "/v3/sessions/create", {
|
|
1181
|
+
method: "POST",
|
|
1182
|
+
body: { custom_title: "gnhf" },
|
|
1183
|
+
signal
|
|
1184
|
+
})).session_id;
|
|
1185
|
+
}
|
|
1186
|
+
async setInlineSystemPrompt(server, sessionId, signal) {
|
|
1187
|
+
const schema = readFileSync(this.schemaPath, "utf-8").trim();
|
|
1188
|
+
await this.requestJSON(server, "/v3/inline-system-prompt", {
|
|
1189
|
+
method: "PUT",
|
|
1190
|
+
sessionId,
|
|
1191
|
+
body: { prompt: buildSystemPrompt(schema) },
|
|
1192
|
+
signal
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async setChatMessage(server, sessionId, prompt, signal) {
|
|
1196
|
+
await this.requestJSON(server, "/v3/set_chat_message", {
|
|
1197
|
+
method: "POST",
|
|
1198
|
+
sessionId,
|
|
1199
|
+
body: { message: prompt },
|
|
1200
|
+
signal
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
async cancelSession(server, sessionId) {
|
|
1204
|
+
try {
|
|
1205
|
+
await this.request(server, "/v3/cancel", {
|
|
1206
|
+
method: "POST",
|
|
1207
|
+
sessionId,
|
|
1208
|
+
timeoutMs: 1e3
|
|
1209
|
+
});
|
|
1210
|
+
} catch {}
|
|
1211
|
+
}
|
|
1212
|
+
async deleteSession(server, sessionId) {
|
|
1213
|
+
try {
|
|
1214
|
+
await this.request(server, `/v3/sessions/${sessionId}`, {
|
|
1215
|
+
method: "DELETE",
|
|
1216
|
+
sessionId,
|
|
1217
|
+
timeoutMs: 1e3
|
|
1218
|
+
});
|
|
1219
|
+
} catch {}
|
|
1220
|
+
}
|
|
1221
|
+
async streamChat(server, sessionId, signal, logStream, onUsage, onMessage) {
|
|
1222
|
+
const response = await this.request(server, "/v3/stream_chat", {
|
|
1223
|
+
method: "GET",
|
|
1224
|
+
sessionId,
|
|
1225
|
+
headers: { accept: "text/event-stream" },
|
|
1226
|
+
signal
|
|
1227
|
+
});
|
|
1228
|
+
if (!response.body) throw new Error("rovodev returned no response body");
|
|
1229
|
+
const usage = {
|
|
1230
|
+
inputTokens: 0,
|
|
1231
|
+
outputTokens: 0,
|
|
1232
|
+
cacheReadTokens: 0,
|
|
1233
|
+
cacheCreationTokens: 0
|
|
1234
|
+
};
|
|
1235
|
+
let latestTextSegment = "";
|
|
1236
|
+
let currentTextParts = [];
|
|
1237
|
+
let currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1238
|
+
const decoder = new TextDecoder();
|
|
1239
|
+
const reader = response.body.getReader();
|
|
1240
|
+
let buffer = "";
|
|
1241
|
+
const emitMessage = () => {
|
|
1242
|
+
const message = currentTextParts.join("").trim();
|
|
1243
|
+
if (message) {
|
|
1244
|
+
latestTextSegment = message;
|
|
1245
|
+
onMessage?.(message);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
const resetCurrentMessage = () => {
|
|
1249
|
+
currentTextParts = [];
|
|
1250
|
+
currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1251
|
+
};
|
|
1252
|
+
const handleUsage = (event) => {
|
|
1253
|
+
usage.inputTokens += event.input_tokens ?? 0;
|
|
1254
|
+
usage.outputTokens += event.output_tokens ?? 0;
|
|
1255
|
+
usage.cacheReadTokens += event.cache_read_tokens ?? 0;
|
|
1256
|
+
usage.cacheCreationTokens += event.cache_write_tokens ?? 0;
|
|
1257
|
+
onUsage?.({ ...usage });
|
|
1258
|
+
};
|
|
1259
|
+
const handleEvent = (rawEvent) => {
|
|
1260
|
+
const lines = rawEvent.split(/\r?\n/);
|
|
1261
|
+
let eventName = "";
|
|
1262
|
+
const dataLines = [];
|
|
1263
|
+
for (const line of lines) {
|
|
1264
|
+
if (line.startsWith("event:")) {
|
|
1265
|
+
eventName = line.slice(6).trim();
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
1269
|
+
}
|
|
1270
|
+
const rawData = dataLines.join("\n");
|
|
1271
|
+
if (rawData.length === 0) return;
|
|
1272
|
+
let payload;
|
|
1273
|
+
try {
|
|
1274
|
+
payload = JSON.parse(rawData);
|
|
1275
|
+
} catch {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const kind = eventName || (typeof payload.event_kind === "string" ? payload.event_kind : "");
|
|
1279
|
+
if (kind === "request-usage") {
|
|
1280
|
+
handleUsage(payload);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (kind === "tool-return" || kind === "on_call_tools_start") {
|
|
1284
|
+
resetCurrentMessage();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (kind === "text") {
|
|
1288
|
+
const content = payload.content;
|
|
1289
|
+
if (typeof content === "string") {
|
|
1290
|
+
currentTextParts = [content];
|
|
1291
|
+
currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1292
|
+
emitMessage();
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (kind === "part_start") {
|
|
1297
|
+
const partStart = payload;
|
|
1298
|
+
if (typeof partStart.index === "number" && partStart.part?.part_kind === "text" && typeof partStart.part.content === "string") {
|
|
1299
|
+
const nextIndex = currentTextParts.push(partStart.part.content) - 1;
|
|
1300
|
+
currentTextIndexes.set(partStart.index, nextIndex);
|
|
1301
|
+
emitMessage();
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (kind === "part_delta") {
|
|
1306
|
+
const partDelta = payload;
|
|
1307
|
+
if (typeof partDelta.index === "number" && partDelta.delta?.part_delta_kind === "text" && typeof partDelta.delta.content_delta === "string") {
|
|
1308
|
+
const textIndex = currentTextIndexes.get(partDelta.index);
|
|
1309
|
+
if (textIndex === void 0) {
|
|
1310
|
+
const nextIndex = currentTextParts.push(partDelta.delta.content_delta) - 1;
|
|
1311
|
+
currentTextIndexes.set(partDelta.index, nextIndex);
|
|
1312
|
+
} else currentTextParts[textIndex] += partDelta.delta.content_delta;
|
|
1313
|
+
emitMessage();
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
while (true) {
|
|
1318
|
+
let readResult;
|
|
1319
|
+
try {
|
|
1320
|
+
readResult = await reader.read();
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (isAbortError(error)) throw createAbortError();
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
if (readResult.done) break;
|
|
1326
|
+
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
1327
|
+
logStream?.write(chunk);
|
|
1328
|
+
buffer += chunk;
|
|
1329
|
+
while (true) {
|
|
1330
|
+
const lfBoundary = buffer.indexOf("\n\n");
|
|
1331
|
+
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
1332
|
+
let boundary;
|
|
1333
|
+
let separatorLen;
|
|
1334
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
1335
|
+
if (crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary)) {
|
|
1336
|
+
boundary = crlfBoundary;
|
|
1337
|
+
separatorLen = 4;
|
|
1338
|
+
} else {
|
|
1339
|
+
boundary = lfBoundary;
|
|
1340
|
+
separatorLen = 2;
|
|
1341
|
+
}
|
|
1342
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
1343
|
+
buffer = buffer.slice(boundary + separatorLen);
|
|
1344
|
+
if (rawEvent.trim()) handleEvent(rawEvent);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
buffer += decoder.decode();
|
|
1348
|
+
if (buffer.trim()) handleEvent(buffer);
|
|
1349
|
+
const finalText = latestTextSegment.trim();
|
|
1350
|
+
if (!finalText) throw new Error("rovodev returned no text output");
|
|
1351
|
+
try {
|
|
1352
|
+
return {
|
|
1353
|
+
output: JSON.parse(finalText),
|
|
1354
|
+
usage
|
|
1355
|
+
};
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
throw new Error(`Failed to parse rovodev output: ${error instanceof Error ? error.message : String(error)}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async shutdownServer() {
|
|
1361
|
+
if (!this.server || this.server.closed) {
|
|
1362
|
+
this.server = null;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (this.closingPromise) {
|
|
1366
|
+
await this.closingPromise;
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const server = this.server;
|
|
1370
|
+
const waitForClose = new Promise((resolve) => {
|
|
1371
|
+
if (server.closed) {
|
|
1372
|
+
resolve();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
server.child.once("close", () => resolve());
|
|
1376
|
+
});
|
|
1377
|
+
try {
|
|
1378
|
+
this.signalServer(server, "SIGTERM");
|
|
1379
|
+
} catch {}
|
|
1380
|
+
const forceKill = new Promise((resolve) => {
|
|
1381
|
+
setTimeout(() => {
|
|
1382
|
+
if (!server.closed) try {
|
|
1383
|
+
this.signalServer(server, "SIGKILL");
|
|
1384
|
+
} catch {}
|
|
1385
|
+
resolve();
|
|
1386
|
+
}, 3e3).unref?.();
|
|
1387
|
+
});
|
|
1388
|
+
this.closingPromise = Promise.race([waitForClose, forceKill]).finally(() => {
|
|
1389
|
+
if (this.server === server) this.server = null;
|
|
1390
|
+
this.closingPromise = null;
|
|
1391
|
+
});
|
|
1392
|
+
await this.closingPromise;
|
|
1393
|
+
}
|
|
1394
|
+
signalServer(server, signal) {
|
|
1395
|
+
if (server.detached && server.child.pid) try {
|
|
1396
|
+
this.killProcessFn(-server.child.pid, signal);
|
|
1397
|
+
return;
|
|
1398
|
+
} catch {}
|
|
1399
|
+
server.child.kill(signal);
|
|
1400
|
+
}
|
|
1401
|
+
async requestJSON(server, path, options) {
|
|
1402
|
+
return await (await this.request(server, path, options)).json();
|
|
1403
|
+
}
|
|
1404
|
+
async request(server, path, options) {
|
|
1405
|
+
const headers = new Headers(options.headers);
|
|
1406
|
+
if (options.sessionId) headers.set("x-session-id", options.sessionId);
|
|
1407
|
+
if (options.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
1408
|
+
const signal = withTimeoutSignal(options.signal, options.timeoutMs);
|
|
1409
|
+
const response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
1410
|
+
method: options.method,
|
|
1411
|
+
headers,
|
|
1412
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
1413
|
+
signal
|
|
1414
|
+
});
|
|
1415
|
+
if (!response.ok) {
|
|
1416
|
+
const body = await response.text();
|
|
1417
|
+
throw new Error(`rovodev ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
1418
|
+
}
|
|
1419
|
+
return response;
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
function withTimeoutSignal(signal, timeoutMs) {
|
|
1423
|
+
if (timeoutMs === void 0) return signal;
|
|
1424
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1425
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
1426
|
+
}
|
|
1427
|
+
//#endregion
|
|
456
1428
|
//#region src/core/agents/factory.ts
|
|
457
1429
|
function createAgent(name, runInfo) {
|
|
458
1430
|
switch (name) {
|
|
459
1431
|
case "claude": return new ClaudeAgent();
|
|
460
1432
|
case "codex": return new CodexAgent(runInfo.schemaPath);
|
|
1433
|
+
case "opencode": return new OpenCodeAgent();
|
|
1434
|
+
case "rovodev": return new RovoDevAgent(runInfo.schemaPath);
|
|
461
1435
|
}
|
|
462
1436
|
}
|
|
463
1437
|
//#endregion
|
|
@@ -495,6 +1469,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
495
1469
|
prompt;
|
|
496
1470
|
limits;
|
|
497
1471
|
stopRequested = false;
|
|
1472
|
+
stopPromise = null;
|
|
498
1473
|
activeAbortController = null;
|
|
499
1474
|
pendingAbortReason = null;
|
|
500
1475
|
state = {
|
|
@@ -528,60 +1503,69 @@ var Orchestrator = class extends EventEmitter {
|
|
|
528
1503
|
stop() {
|
|
529
1504
|
this.stopRequested = true;
|
|
530
1505
|
this.activeAbortController?.abort();
|
|
531
|
-
|
|
532
|
-
this.
|
|
533
|
-
|
|
534
|
-
|
|
1506
|
+
if (this.stopPromise) return;
|
|
1507
|
+
this.stopPromise = (async () => {
|
|
1508
|
+
await this.closeAgent();
|
|
1509
|
+
resetHard(this.cwd);
|
|
1510
|
+
this.state.status = "stopped";
|
|
1511
|
+
this.emit("state", this.getState());
|
|
1512
|
+
this.emit("stopped");
|
|
1513
|
+
})();
|
|
535
1514
|
}
|
|
536
1515
|
async start() {
|
|
537
1516
|
this.state.startTime = /* @__PURE__ */ new Date();
|
|
538
1517
|
this.state.status = "running";
|
|
539
1518
|
this.emit("state", this.getState());
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const iterationPrompt = buildIterationPrompt({
|
|
551
|
-
n: this.state.currentIteration,
|
|
552
|
-
runId: this.runInfo.runId,
|
|
553
|
-
prompt: this.prompt
|
|
554
|
-
});
|
|
555
|
-
const result = await this.runIteration(iterationPrompt);
|
|
556
|
-
if (result.type === "aborted") {
|
|
557
|
-
this.abort(result.reason);
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
const { record } = result;
|
|
561
|
-
this.state.iterations.push(record);
|
|
562
|
-
this.emit("iteration:end", record);
|
|
563
|
-
this.emit("state", this.getState());
|
|
564
|
-
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
565
|
-
if (postIterationAbortReason) {
|
|
566
|
-
this.abort(postIterationAbortReason);
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
if (this.state.consecutiveFailures >= this.config.maxConsecutiveFailures) {
|
|
570
|
-
this.abort(`${this.config.maxConsecutiveFailures} consecutive failures`);
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
if (this.state.consecutiveFailures > 0 && !this.stopRequested) {
|
|
574
|
-
const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveFailures - 1);
|
|
575
|
-
this.state.status = "waiting";
|
|
576
|
-
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
1519
|
+
try {
|
|
1520
|
+
while (!this.stopRequested) {
|
|
1521
|
+
const preIterationAbortReason = this.getPreIterationAbortReason();
|
|
1522
|
+
if (preIterationAbortReason) {
|
|
1523
|
+
this.abort(preIterationAbortReason);
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
this.state.currentIteration++;
|
|
1527
|
+
this.state.status = "running";
|
|
1528
|
+
this.emit("iteration:start", this.state.currentIteration);
|
|
577
1529
|
this.emit("state", this.getState());
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.
|
|
1530
|
+
const iterationPrompt = buildIterationPrompt({
|
|
1531
|
+
n: this.state.currentIteration,
|
|
1532
|
+
runId: this.runInfo.runId,
|
|
1533
|
+
prompt: this.prompt
|
|
1534
|
+
});
|
|
1535
|
+
const result = await this.runIteration(iterationPrompt);
|
|
1536
|
+
if (result.type === "aborted") {
|
|
1537
|
+
this.abort(result.reason);
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
const { record } = result;
|
|
1541
|
+
this.state.iterations.push(record);
|
|
1542
|
+
this.emit("iteration:end", record);
|
|
1543
|
+
this.emit("state", this.getState());
|
|
1544
|
+
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
1545
|
+
if (postIterationAbortReason) {
|
|
1546
|
+
this.abort(postIterationAbortReason);
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
if (this.state.consecutiveFailures >= this.config.maxConsecutiveFailures) {
|
|
1550
|
+
this.abort(`${this.config.maxConsecutiveFailures} consecutive failures`);
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
if (this.state.consecutiveFailures > 0 && !this.stopRequested) {
|
|
1554
|
+
const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveFailures - 1);
|
|
1555
|
+
this.state.status = "waiting";
|
|
1556
|
+
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
582
1557
|
this.emit("state", this.getState());
|
|
1558
|
+
await this.interruptibleSleep(backoffMs);
|
|
1559
|
+
this.state.waitingUntil = null;
|
|
1560
|
+
if (!this.stopRequested) {
|
|
1561
|
+
this.state.status = "running";
|
|
1562
|
+
this.emit("state", this.getState());
|
|
1563
|
+
}
|
|
583
1564
|
}
|
|
584
1565
|
}
|
|
1566
|
+
} finally {
|
|
1567
|
+
if (this.stopPromise) await this.stopPromise;
|
|
1568
|
+
else await this.closeAgent();
|
|
585
1569
|
}
|
|
586
1570
|
}
|
|
587
1571
|
async runIteration(prompt) {
|
|
@@ -701,6 +1685,11 @@ var Orchestrator = class extends EventEmitter {
|
|
|
701
1685
|
this.emit("abort", reason);
|
|
702
1686
|
this.emit("state", this.getState());
|
|
703
1687
|
}
|
|
1688
|
+
async closeAgent() {
|
|
1689
|
+
try {
|
|
1690
|
+
await this.agent.close?.();
|
|
1691
|
+
} catch {}
|
|
1692
|
+
}
|
|
704
1693
|
};
|
|
705
1694
|
//#endregion
|
|
706
1695
|
//#region src/mock-orchestrator.ts
|
|
@@ -1030,9 +2019,17 @@ const MOON_PHASE_PERIOD = 1600;
|
|
|
1030
2019
|
const MAX_MSG_LINES = 3;
|
|
1031
2020
|
const MAX_MSG_LINE_LEN = 64;
|
|
1032
2021
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
1033
|
-
function
|
|
2022
|
+
function spacedLabel(text) {
|
|
2023
|
+
return text.split("").join(" ");
|
|
2024
|
+
}
|
|
2025
|
+
function renderTitleCells(agentName) {
|
|
1034
2026
|
return [
|
|
1035
|
-
textToCells("
|
|
2027
|
+
[...textToCells(spacedLabel("gnhf"), "dim"), ...agentName ? [
|
|
2028
|
+
...textToCells(" ", "normal"),
|
|
2029
|
+
...textToCells("·", "dim"),
|
|
2030
|
+
...textToCells(" ", "normal"),
|
|
2031
|
+
...textToCells(spacedLabel(agentName), "dim")
|
|
2032
|
+
] : []],
|
|
1036
2033
|
[],
|
|
1037
2034
|
textToCells("┏━╸┏━┓┏━┓╺┳┓ ┏┓╻╻┏━╸╻ ╻╺┳╸ ╻ ╻┏━┓╻ ╻┏━╸ ┏━╸╻ ╻┏┓╻", "bold"),
|
|
1038
2035
|
textToCells("┃╺┓┃ ┃┃ ┃ ┃┃ ┃┗┫┃┃╺┓┣━┫ ┃ ┣━┫┣━┫┃┏┛┣╸ ┣╸ ┃ ┃┃┗┫", "bold"),
|
|
@@ -1137,11 +2134,11 @@ function fitContentRows(contentRows, maxRows) {
|
|
|
1137
2134
|
}
|
|
1138
2135
|
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
1139
2136
|
}
|
|
1140
|
-
function buildContentCells(prompt, state, elapsed, now) {
|
|
2137
|
+
function buildContentCells(prompt, agentName, state, elapsed, now) {
|
|
1141
2138
|
const rows = [];
|
|
1142
2139
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
1143
2140
|
rows.push([]);
|
|
1144
|
-
rows.push(...renderTitleCells());
|
|
2141
|
+
rows.push(...renderTitleCells(agentName));
|
|
1145
2142
|
rows.push([], []);
|
|
1146
2143
|
const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
|
|
1147
2144
|
for (let i = 0; i < MAX_PROMPT_LINES; i++) {
|
|
@@ -1156,10 +2153,10 @@ function buildContentCells(prompt, state, elapsed, now) {
|
|
|
1156
2153
|
rows.push(...renderMoonStripCells(state.iterations, isRunning, now));
|
|
1157
2154
|
return rows;
|
|
1158
2155
|
}
|
|
1159
|
-
function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
2156
|
+
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
1160
2157
|
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
1161
2158
|
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
1162
|
-
const contentRows = fitContentRows(buildContentCells(prompt, state, elapsed, now), availableHeight);
|
|
2159
|
+
const contentRows = fitContentRows(buildContentCells(prompt, agentName, state, elapsed, now), availableHeight);
|
|
1163
2160
|
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
1164
2161
|
const contentCount = contentRows.length;
|
|
1165
2162
|
const remaining = Math.max(0, availableHeight - contentCount);
|
|
@@ -1186,6 +2183,7 @@ function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, t
|
|
|
1186
2183
|
var Renderer = class {
|
|
1187
2184
|
orchestrator;
|
|
1188
2185
|
prompt;
|
|
2186
|
+
agentName;
|
|
1189
2187
|
state;
|
|
1190
2188
|
interval = null;
|
|
1191
2189
|
exitResolve;
|
|
@@ -1197,9 +2195,10 @@ var Renderer = class {
|
|
|
1197
2195
|
cachedHeight = 0;
|
|
1198
2196
|
prevCells = [];
|
|
1199
2197
|
isFirstFrame = true;
|
|
1200
|
-
constructor(orchestrator, prompt) {
|
|
2198
|
+
constructor(orchestrator, prompt, agentName) {
|
|
1201
2199
|
this.orchestrator = orchestrator;
|
|
1202
2200
|
this.prompt = prompt;
|
|
2201
|
+
this.agentName = agentName;
|
|
1203
2202
|
this.state = orchestrator.getState();
|
|
1204
2203
|
this.exitPromise = new Promise((resolve) => {
|
|
1205
2204
|
this.exitResolve = resolve;
|
|
@@ -1219,7 +2218,10 @@ var Renderer = class {
|
|
|
1219
2218
|
process$1.stdin.setRawMode(true);
|
|
1220
2219
|
process$1.stdin.resume();
|
|
1221
2220
|
process$1.stdin.on("data", (data) => {
|
|
1222
|
-
if (data[0] === 3)
|
|
2221
|
+
if (data[0] === 3) {
|
|
2222
|
+
this.stop();
|
|
2223
|
+
this.orchestrator.stop();
|
|
2224
|
+
}
|
|
1223
2225
|
});
|
|
1224
2226
|
}
|
|
1225
2227
|
this.interval = setInterval(() => this.render(), TICK_MS);
|
|
@@ -1273,7 +2275,7 @@ var Renderer = class {
|
|
|
1273
2275
|
const w = process$1.stdout.columns || 80;
|
|
1274
2276
|
const h = process$1.stdout.rows || 24;
|
|
1275
2277
|
const resized = this.ensureStarFields(w, h);
|
|
1276
|
-
const nextCells = buildFrameCells(this.prompt, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
|
|
2278
|
+
const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
|
|
1277
2279
|
if (this.isFirstFrame || resized) {
|
|
1278
2280
|
process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
|
|
1279
2281
|
this.isFirstFrame = false;
|
|
@@ -1292,6 +2294,7 @@ function slugifyPrompt(prompt) {
|
|
|
1292
2294
|
//#endregion
|
|
1293
2295
|
//#region src/cli.ts
|
|
1294
2296
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
2297
|
+
const FORCE_EXIT_TIMEOUT_MS = 5e3;
|
|
1295
2298
|
function parseNonNegativeInteger(value) {
|
|
1296
2299
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
1297
2300
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -1323,11 +2326,11 @@ function ask(question) {
|
|
|
1323
2326
|
});
|
|
1324
2327
|
}
|
|
1325
2328
|
const program = new Command();
|
|
1326
|
-
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
|
|
2329
|
+
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, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--mock", "", false).action(async (promptArg, options) => {
|
|
1327
2330
|
if (options.mock) {
|
|
1328
2331
|
const mock = new MockOrchestrator();
|
|
1329
2332
|
enterAltScreen();
|
|
1330
|
-
const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality");
|
|
2333
|
+
const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude");
|
|
1331
2334
|
renderer.start();
|
|
1332
2335
|
mock.start();
|
|
1333
2336
|
await renderer.waitUntilExit();
|
|
@@ -1337,13 +2340,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1337
2340
|
let prompt = promptArg;
|
|
1338
2341
|
if (!prompt && !process$1.stdin.isTTY) prompt = readFileSync("/dev/stdin", "utf-8").trim();
|
|
1339
2342
|
const agentName = options.agent;
|
|
1340
|
-
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex") {
|
|
1341
|
-
console.error(`Unknown agent: ${options.agent}. Use "claude" or "
|
|
2343
|
+
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex" && agentName !== "rovodev" && agentName !== "opencode") {
|
|
2344
|
+
console.error(`Unknown agent: ${options.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
1342
2345
|
process$1.exit(1);
|
|
1343
2346
|
}
|
|
1344
2347
|
const config = loadConfig(agentName ? { agent: agentName } : void 0);
|
|
1345
|
-
if (config.agent !== "claude" && config.agent !== "codex") {
|
|
1346
|
-
console.error(`Unknown agent: ${config.agent}. Use "claude" or "
|
|
2348
|
+
if (config.agent !== "claude" && config.agent !== "codex" && config.agent !== "rovodev" && config.agent !== "opencode") {
|
|
2349
|
+
console.error(`Unknown agent: ${config.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
1347
2350
|
process$1.exit(1);
|
|
1348
2351
|
}
|
|
1349
2352
|
const cwd = process$1.cwd();
|
|
@@ -1379,15 +2382,22 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1379
2382
|
maxTokens: options.maxTokens
|
|
1380
2383
|
});
|
|
1381
2384
|
enterAltScreen();
|
|
1382
|
-
const renderer = new Renderer(orchestrator, prompt);
|
|
2385
|
+
const renderer = new Renderer(orchestrator, prompt, config.agent);
|
|
1383
2386
|
renderer.start();
|
|
1384
|
-
orchestrator.start().
|
|
2387
|
+
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
1385
2388
|
renderer.stop();
|
|
2389
|
+
}).catch((err) => {
|
|
1386
2390
|
exitAltScreen();
|
|
1387
2391
|
die(err instanceof Error ? err.message : String(err));
|
|
1388
2392
|
});
|
|
1389
2393
|
await renderer.waitUntilExit();
|
|
1390
2394
|
exitAltScreen();
|
|
2395
|
+
if (await Promise.race([orchestratorPromise.then(() => "done"), new Promise((resolve) => {
|
|
2396
|
+
setTimeout(() => resolve("timeout"), FORCE_EXIT_TIMEOUT_MS).unref();
|
|
2397
|
+
})]) === "timeout") {
|
|
2398
|
+
console.error(`\n gnhf: shutdown timed out after ${FORCE_EXIT_TIMEOUT_MS / 1e3}s, forcing exit\n`);
|
|
2399
|
+
process$1.exit(130);
|
|
2400
|
+
}
|
|
1391
2401
|
});
|
|
1392
2402
|
function enterAltScreen() {
|
|
1393
2403
|
process$1.stdout.write("\x1B[?1049h");
|