@proagentstore/cli 0.2.2 → 0.2.4
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.
|
@@ -10,10 +10,28 @@ export class HeadlessSession {
|
|
|
10
10
|
run = "idle";
|
|
11
11
|
/** Claude Code's own session id (from the init event) — used to --resume. */
|
|
12
12
|
claudeSessionId = null;
|
|
13
|
+
/** "stream-json" for Claude (structured) · "raw" for any other CLI (stdout capture). */
|
|
14
|
+
mode;
|
|
15
|
+
cmdBin;
|
|
16
|
+
cmdArgs;
|
|
17
|
+
binName;
|
|
18
|
+
/** Wall-clock of the last stdout/stderr byte — drives the raw-mode idle heuristic. */
|
|
19
|
+
lastOutputAt = 0;
|
|
20
|
+
/** Raw-mode: has any output arrived since the current turn's input? */
|
|
21
|
+
sawOutputSinceInput = false;
|
|
22
|
+
/** Raw-mode: when the current turn started (absolute idle backstop). */
|
|
23
|
+
turnStartedAt = 0;
|
|
13
24
|
constructor(config) {
|
|
14
25
|
this.config = config;
|
|
15
26
|
this.sessionName = `pags-${config.clientType}-${config.id}`;
|
|
16
27
|
this.claudeSessionId = readState(config.statePath, config.id);
|
|
28
|
+
// Claude is the structured engine; everything else is a raw CLI.
|
|
29
|
+
this.mode = config.clientType === "claude" ? "stream-json" : "raw";
|
|
30
|
+
const { bin, args } = parseCommand(config.command);
|
|
31
|
+
// A test/override bin wins for the binary only; the command's args are kept.
|
|
32
|
+
this.cmdBin = config.bin ?? (bin || "claude");
|
|
33
|
+
this.cmdArgs = args;
|
|
34
|
+
this.binName = (this.cmdBin.split("/").pop() || this.cmdBin) || "cli";
|
|
17
35
|
}
|
|
18
36
|
/** True while the agent process is running. */
|
|
19
37
|
get alive() {
|
|
@@ -21,10 +39,30 @@ export class HeadlessSession {
|
|
|
21
39
|
}
|
|
22
40
|
/** Idle = ready for the next instruction. */
|
|
23
41
|
get ready() {
|
|
24
|
-
return this.alive && this.
|
|
42
|
+
return this.alive && this.runState() === "idle";
|
|
25
43
|
}
|
|
26
44
|
runState() {
|
|
27
|
-
|
|
45
|
+
if (!this.alive)
|
|
46
|
+
return "idle";
|
|
47
|
+
// Raw engines have no "turn over" event, so we INFER idle. Three rules, in order:
|
|
48
|
+
// 1. produced output, then went quiet for 1.5s → settled (the common case).
|
|
49
|
+
// 2. NEVER produced output but 8s elapsed → a silent turn (just a prompt) is done.
|
|
50
|
+
// 3. absolute 15-min backstop → never wedge "thinking" forever (e.g. a heartbeat
|
|
51
|
+
// that keeps refreshing lastOutputAt). The user can also Restart from diagnostics.
|
|
52
|
+
// Rule 1 deliberately waits for first output so a slow first token can't flip us
|
|
53
|
+
// to idle mid-turn (which would make the brain proceed against a half-done turn).
|
|
54
|
+
if (this.mode === "raw" && this.run === "thinking") {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const quiet = now - this.lastOutputAt;
|
|
57
|
+
const elapsed = now - this.turnStartedAt;
|
|
58
|
+
if (this.sawOutputSinceInput && quiet > 1500)
|
|
59
|
+
this.run = "idle";
|
|
60
|
+
else if (!this.sawOutputSinceInput && elapsed > 8000)
|
|
61
|
+
this.run = "idle";
|
|
62
|
+
else if (elapsed > 15 * 60 * 1000)
|
|
63
|
+
this.run = "idle";
|
|
64
|
+
}
|
|
65
|
+
return this.run === "thinking" ? "thinking" : "idle";
|
|
28
66
|
}
|
|
29
67
|
/** The rendered conversation the brain/console reads (Claude's real output). */
|
|
30
68
|
snapshot() {
|
|
@@ -34,43 +72,38 @@ export class HeadlessSession {
|
|
|
34
72
|
start() {
|
|
35
73
|
if (this.alive)
|
|
36
74
|
return;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"stream-json",
|
|
43
|
-
"--verbose",
|
|
44
|
-
"--dangerously-skip-permissions",
|
|
45
|
-
];
|
|
46
|
-
// Continue the prior conversation after a runner restart (state in ~/.claude).
|
|
47
|
-
if (this.claudeSessionId)
|
|
48
|
-
args.push("--resume", this.claudeSessionId);
|
|
49
|
-
this.proc = spawn(this.config.bin ?? "claude", args, {
|
|
75
|
+
// stream-json: we own the structural flags + --resume; merge the user's extras
|
|
76
|
+
// (e.g. --model) without letting them clobber or orphan-value our flags. raw:
|
|
77
|
+
// run exactly what the user configured and capture stdout.
|
|
78
|
+
const args = this.mode === "stream-json" ? buildClaudeArgs(this.cmdArgs, this.claudeSessionId) : [...this.cmdArgs];
|
|
79
|
+
this.proc = spawn(this.cmdBin, args, {
|
|
50
80
|
cwd: this.config.workDir,
|
|
51
81
|
env: { ...process.env, ...this.config.env },
|
|
52
82
|
stdio: ["pipe", "pipe", "pipe"],
|
|
53
83
|
});
|
|
54
84
|
this.run = "idle";
|
|
55
|
-
|
|
85
|
+
this.lastOutputAt = Date.now();
|
|
86
|
+
// MUST handle 'error' — without a listener, a spawn failure (e.g. the binary
|
|
56
87
|
// not on PATH) is thrown as an uncaught exception and crashes the runner.
|
|
57
88
|
this.proc.on("error", (err) => {
|
|
58
89
|
this.run = "idle";
|
|
59
|
-
this.push(`[cannot run \`${this.
|
|
90
|
+
this.push(`[cannot run \`${this.cmdBin}\`: ${err.message} — is ${this.binName} installed and on your PATH?]`);
|
|
60
91
|
this.proc = null;
|
|
61
92
|
});
|
|
62
93
|
// Swallow EPIPE when writing to a process that just exited (one-shot turn).
|
|
63
94
|
this.proc.stdin?.on("error", () => { });
|
|
64
95
|
this.proc.stdout?.on("data", (d) => this.onStdout(d.toString("utf8")));
|
|
65
96
|
this.proc.stderr?.on("data", (d) => {
|
|
97
|
+
this.lastOutputAt = Date.now();
|
|
98
|
+
this.sawOutputSinceInput = true;
|
|
66
99
|
const text = d.toString("utf8").trim();
|
|
67
100
|
if (text)
|
|
68
|
-
this.push(`[
|
|
101
|
+
this.push(`[${this.binName}] ${stripAnsi(text)}`);
|
|
69
102
|
});
|
|
70
103
|
this.proc.on("exit", (code) => {
|
|
71
104
|
this.run = "idle";
|
|
72
105
|
if (code && code !== 0)
|
|
73
|
-
this.push(`[
|
|
106
|
+
this.push(`[${this.binName} exited with code ${code}]`);
|
|
74
107
|
// A bad --resume can kill the process instantly; drop it so the next
|
|
75
108
|
// start is a clean session rather than looping on a dead id.
|
|
76
109
|
if (code && code !== 0 && this.claudeSessionId) {
|
|
@@ -79,15 +112,25 @@ export class HeadlessSession {
|
|
|
79
112
|
}
|
|
80
113
|
});
|
|
81
114
|
}
|
|
82
|
-
/** Send a user turn to
|
|
115
|
+
/** Send a user turn to the agent (it acts on it). */
|
|
83
116
|
input(text) {
|
|
84
117
|
if (!this.alive)
|
|
85
118
|
this.start();
|
|
86
119
|
this.push(`\n❯ [${stamp()}] ${text}`); // ❯ — your turn, timestamped
|
|
87
120
|
this.run = "thinking";
|
|
88
|
-
const
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
this.lastOutputAt = now;
|
|
123
|
+
this.turnStartedAt = now;
|
|
124
|
+
this.sawOutputSinceInput = false; // arm the raw idle heuristic for THIS turn
|
|
89
125
|
try {
|
|
90
|
-
this.
|
|
126
|
+
if (this.mode === "stream-json") {
|
|
127
|
+
const msg = JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text }] } });
|
|
128
|
+
this.proc?.stdin?.write(`${msg}\n`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Raw CLI: write the line to stdin as if typed.
|
|
132
|
+
this.proc?.stdin?.write(`${text}\n`);
|
|
133
|
+
}
|
|
91
134
|
}
|
|
92
135
|
catch {
|
|
93
136
|
/* process may have died; the next snapshot reports not-alive */
|
|
@@ -120,16 +163,37 @@ export class HeadlessSession {
|
|
|
120
163
|
}
|
|
121
164
|
// ── internals ────────────────────────────────────────────────────────────
|
|
122
165
|
onStdout(chunk) {
|
|
166
|
+
this.lastOutputAt = Date.now();
|
|
167
|
+
this.sawOutputSinceInput = true; // a slow first token must NOT count as idle
|
|
123
168
|
this.buf += chunk;
|
|
124
169
|
let nl;
|
|
125
170
|
// biome-ignore lint/suspicious/noAssignInExpressions: standard line-buffer drain
|
|
126
171
|
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
127
172
|
const line = this.buf.slice(0, nl).trim();
|
|
128
173
|
this.buf = this.buf.slice(nl + 1);
|
|
129
|
-
if (line)
|
|
174
|
+
if (!line)
|
|
175
|
+
continue;
|
|
176
|
+
if (this.mode === "stream-json")
|
|
130
177
|
this.handle(line);
|
|
178
|
+
else
|
|
179
|
+
this.pushRaw(line); // raw engine — the line IS the terminal output
|
|
180
|
+
}
|
|
181
|
+
// A TUI/raw engine may render without newlines; don't let `buf` grow unbounded
|
|
182
|
+
// (and surface the partial output so the pane isn't blank). 16KB is generous.
|
|
183
|
+
if (this.buf.length > 16 * 1024) {
|
|
184
|
+
if (this.mode === "raw")
|
|
185
|
+
this.pushRaw(this.buf);
|
|
186
|
+
this.buf = "";
|
|
131
187
|
}
|
|
132
188
|
}
|
|
189
|
+
/** Raw-engine stdout: strip ANSI control codes and append to the transcript. */
|
|
190
|
+
pushRaw(line) {
|
|
191
|
+
const clean = stripAnsi(line);
|
|
192
|
+
if (clean.trim())
|
|
193
|
+
this.push(clean);
|
|
194
|
+
if (this.transcript.length > 4000)
|
|
195
|
+
this.transcript = this.transcript.slice(-3000);
|
|
196
|
+
}
|
|
133
197
|
handle(line) {
|
|
134
198
|
let ev;
|
|
135
199
|
try {
|
|
@@ -181,6 +245,49 @@ export class HeadlessSession {
|
|
|
181
245
|
function stamp() {
|
|
182
246
|
return new Date().toTimeString().slice(0, 8);
|
|
183
247
|
}
|
|
248
|
+
/** Split a launch command into bin + args, respecting single/double quotes. */
|
|
249
|
+
export function parseCommand(command) {
|
|
250
|
+
const tokens = [];
|
|
251
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
252
|
+
let m;
|
|
253
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: tokenizer drain
|
|
254
|
+
while ((m = re.exec(command ?? "")) !== null)
|
|
255
|
+
tokens.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
256
|
+
return { bin: tokens[0] ?? "", args: tokens.slice(1) };
|
|
257
|
+
}
|
|
258
|
+
/** Structural flags PAGS owns for the Claude stream-json engine — a user command
|
|
259
|
+
* must not override or duplicate these (and must not orphan their values). */
|
|
260
|
+
const RESERVED_CLAUDE_FLAGS = new Set(["-p", "--print", "--input-format", "--output-format", "--verbose", "--resume"]);
|
|
261
|
+
/**
|
|
262
|
+
* Build Claude's argv: our structural stream-json flags + the user's extra args
|
|
263
|
+
* (e.g. `--model`), with reserved flags (and their values) stripped so the user
|
|
264
|
+
* can't clobber the protocol or leave an orphaned positional. `--resume` is added
|
|
265
|
+
* last from our persisted session id, never from the user's command.
|
|
266
|
+
*/
|
|
267
|
+
export function buildClaudeArgs(userArgs, resumeId) {
|
|
268
|
+
const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"];
|
|
269
|
+
for (let i = 0; i < userArgs.length; i++) {
|
|
270
|
+
const a = userArgs[i];
|
|
271
|
+
if (RESERVED_CLAUDE_FLAGS.has(a)) {
|
|
272
|
+
// drop the flag AND its value (when the next token isn't itself a flag)
|
|
273
|
+
if (i + 1 < userArgs.length && !userArgs[i + 1].startsWith("-"))
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!args.includes(a))
|
|
278
|
+
args.push(a);
|
|
279
|
+
}
|
|
280
|
+
if (!args.includes("--dangerously-skip-permissions"))
|
|
281
|
+
args.push("--dangerously-skip-permissions");
|
|
282
|
+
if (resumeId)
|
|
283
|
+
args.push("--resume", resumeId);
|
|
284
|
+
return args;
|
|
285
|
+
}
|
|
286
|
+
/** Strip ANSI/VT escape sequences so a raw CLI's coloured output reads as plain text. */
|
|
287
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping terminal escape codes
|
|
288
|
+
function stripAnsi(s) {
|
|
289
|
+
return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b[()][AB0-2]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
290
|
+
}
|
|
184
291
|
/** Compact a tool_use input object to a single readable line. */
|
|
185
292
|
function shortInput(input) {
|
|
186
293
|
if (input == null)
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
2
|
+
"name": "@proagentstore/cli",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "CLI for creating, publishing, and running ProAgentStore agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pags": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ProAgentStore/platform#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ProAgentStore/platform.git",
|
|
14
|
+
"directory": "packages/cli"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"proagentstore",
|
|
18
|
+
"pags",
|
|
19
|
+
"cli",
|
|
20
|
+
"agents",
|
|
21
|
+
"mcp",
|
|
22
|
+
"browser-runner"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "pnpm --filter @proagentstore/browser-runner build && tsup src/index.ts --format esm --dts && node scripts/copy-browser-runner.mjs",
|
|
29
|
+
"dev": "tsx src/index.ts",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chalk": "^5.6.2",
|
|
34
|
+
"commander": "^13.0.0",
|
|
35
|
+
"playwright": "^1.60.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"tsup": "^8.4.0",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.7.0"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
]
|
|
46
46
|
}
|