@proagentstore/cli 0.4.22 → 0.4.24
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.
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
4
|
+
* REMOVE rather than "set to empty".
|
|
5
|
+
*
|
|
6
|
+
* Needed because the machine env is inherited wholesale: a developer with ANTHROPIC_API_KEY in
|
|
7
|
+
* their shell handed it to every engine, and Claude Code prefers an API key over the
|
|
8
|
+
* subscription token — so choosing "subscription" injected CLAUDE_CODE_OAUTH_TOKEN and then
|
|
9
|
+
* silently lost, billing per token anyway. Without a way to express removal the setting could
|
|
10
|
+
* not mean what it said.
|
|
11
|
+
*/
|
|
12
|
+
export function mergeEnv(base, overlay) {
|
|
13
|
+
const out = { ...base };
|
|
14
|
+
for (const [k, v] of Object.entries(overlay ?? {})) {
|
|
15
|
+
if (v === "")
|
|
16
|
+
delete out[k];
|
|
17
|
+
else
|
|
18
|
+
out[k] = v;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
2
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
23
|
import { dirname, join } from "node:path";
|
|
4
24
|
import { handlerFor } from "./handlers.js";
|
|
@@ -86,7 +106,31 @@ export class HeadlessSession {
|
|
|
86
106
|
return this.transcript.join("\n");
|
|
87
107
|
}
|
|
88
108
|
/** Launch (or resume) the agent process. Idempotent if already alive. */
|
|
109
|
+
/**
|
|
110
|
+
* Raw engines run ONE-SHOT PER TURN, not as a persistent interactive process.
|
|
111
|
+
*
|
|
112
|
+
* The persistent design assumed an interactive CLI reading stdin — which is what tmux used to
|
|
113
|
+
* provide, via a PTY. Without one, `codex` (and grok, and any TUI) dies immediately with
|
|
114
|
+
* "stdin is not a terminal", so three of the four engines were simply broken.
|
|
115
|
+
*
|
|
116
|
+
* AgentCoder solved this before PAGS existed and ran fine on it: `claude -p '<instruction>'`
|
|
117
|
+
* per turn, plain exec, no PTY and no tmux. Codex's exact analogue is `codex exec`. So the
|
|
118
|
+
* process is spawned when a turn ARRIVES and exits when the turn ends, rather than being
|
|
119
|
+
* kept alive for stdin writes that a non-interactive binary will never read.
|
|
120
|
+
*
|
|
121
|
+
* Claude keeps its persistent stream-json process — it is explicitly non-interactive and
|
|
122
|
+
* multi-turn, which is why it survived the migration untouched.
|
|
123
|
+
*/
|
|
124
|
+
get oneShot() {
|
|
125
|
+
return this.mode === "raw";
|
|
126
|
+
}
|
|
89
127
|
start() {
|
|
128
|
+
// A one-shot engine has nothing to start until there is a turn to run; starting it here
|
|
129
|
+
// is what produced the instant "exited with code 1".
|
|
130
|
+
if (this.oneShot) {
|
|
131
|
+
this.run = "idle";
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
90
134
|
if (this.alive)
|
|
91
135
|
return;
|
|
92
136
|
// stream-json: we own the structural flags + --resume; merge the user's extras
|
|
@@ -95,7 +139,7 @@ export class HeadlessSession {
|
|
|
95
139
|
const args = this.mode === "stream-json" ? buildClaudeArgs(this.cmdArgs, this.claudeSessionId) : [...this.cmdArgs];
|
|
96
140
|
const proc = spawn(this.cmdBin, args, {
|
|
97
141
|
cwd: this.config.workDir,
|
|
98
|
-
env:
|
|
142
|
+
env: mergeEnv(process.env, this.config.env),
|
|
99
143
|
stdio: ["pipe", "pipe", "pipe"],
|
|
100
144
|
});
|
|
101
145
|
this.proc = proc;
|
|
@@ -153,14 +197,56 @@ export class HeadlessSession {
|
|
|
153
197
|
this.proc?.stdin?.write(`${msg}\n`);
|
|
154
198
|
}
|
|
155
199
|
else {
|
|
156
|
-
// Raw CLI:
|
|
157
|
-
|
|
200
|
+
// Raw CLI: spawn THIS turn. See `oneShot` — there is no persistent process to
|
|
201
|
+
// write to, because a non-interactive binary would never have read it.
|
|
202
|
+
this.runOneShot(text);
|
|
158
203
|
}
|
|
159
204
|
}
|
|
160
205
|
catch {
|
|
161
206
|
/* process may have died; the next snapshot reports not-alive */
|
|
162
207
|
}
|
|
163
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Run ONE turn as its own process, the way AgentCoder did (`claude -p '<instruction>'`).
|
|
211
|
+
*
|
|
212
|
+
* Contract, deliberately general so it is not a Codex special case: **the preset command is a
|
|
213
|
+
* prefix and the turn text is appended as the final argument.** That makes every prompt-in /
|
|
214
|
+
* text-out CLI usable by configuration alone —
|
|
215
|
+
*
|
|
216
|
+
* codex exec → codex exec "<turn>"
|
|
217
|
+
* claude -p → claude -p "<turn>"
|
|
218
|
+
* ollama run llama3 → ollama run llama3 "<turn>"
|
|
219
|
+
* my-model --flag → my-model --flag "<turn>"
|
|
220
|
+
*
|
|
221
|
+
* — including a local model, with no platform change and no cloud key. Whoever configures the
|
|
222
|
+
* preset decides what runs and what it costs; the platform does not need to know the engine.
|
|
223
|
+
*/
|
|
224
|
+
runOneShot(text) {
|
|
225
|
+
const proc = spawn(this.cmdBin, [...this.cmdArgs, text], {
|
|
226
|
+
cwd: this.config.workDir,
|
|
227
|
+
env: mergeEnv(process.env, this.config.env),
|
|
228
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
229
|
+
});
|
|
230
|
+
this.proc = proc;
|
|
231
|
+
// Without an 'error' listener a spawn failure (binary not on PATH) is an uncaught
|
|
232
|
+
// exception that takes the whole runner down, not just this session.
|
|
233
|
+
proc.on("error", (err) => {
|
|
234
|
+
this.push(`[${this.config.clientType}] failed to start: ${err.message}`);
|
|
235
|
+
this.run = "idle";
|
|
236
|
+
this.proc = null;
|
|
237
|
+
});
|
|
238
|
+
proc.stdout?.on("data", (d) => this.onStdout(d.toString()));
|
|
239
|
+
proc.stderr?.on("data", (d) => this.onStdout(d.toString()));
|
|
240
|
+
proc.on("close", (code) => {
|
|
241
|
+
// A non-zero exit is the engine's own failure (bad flags, not signed in) and the
|
|
242
|
+
// operator needs to see it — silently going idle is how "stdin is not a terminal"
|
|
243
|
+
// looked like an idle session for a whole afternoon.
|
|
244
|
+
if (code)
|
|
245
|
+
this.push(`[${this.config.clientType} exited with code ${code}]`);
|
|
246
|
+
this.run = "idle";
|
|
247
|
+
this.proc = null;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
164
250
|
/** No TTY in headless mode; control is via messages. Kept for interface parity. */
|
|
165
251
|
key(_keys) {
|
|
166
252
|
/* intentionally a no-op — there are no raw keystrokes without a terminal */
|