pullfrog 0.1.0 → 0.1.1
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/dist/cli.mjs +48 -8
- package/dist/index.js +47 -7
- package/dist/utils/subprocess.d.ts +2 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -109979,6 +109979,7 @@ async function spawn(options) {
|
|
|
109979
109979
|
const startTime = performance3.now();
|
|
109980
109980
|
let stdoutBuffer = "";
|
|
109981
109981
|
let stderrBuffer = "";
|
|
109982
|
+
const killGroup = options.killGroup ?? false;
|
|
109982
109983
|
return new Promise((resolve3, reject) => {
|
|
109983
109984
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
109984
109985
|
env: options.env || {
|
|
@@ -109986,9 +109987,20 @@ async function spawn(options) {
|
|
|
109986
109987
|
HOME: process.env.HOME || ""
|
|
109987
109988
|
},
|
|
109988
109989
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
|
109989
|
-
cwd: options.cwd || process.cwd()
|
|
109990
|
+
cwd: options.cwd || process.cwd(),
|
|
109991
|
+
detached: killGroup
|
|
109990
109992
|
});
|
|
109991
|
-
|
|
109993
|
+
const killSelf = (signal) => {
|
|
109994
|
+
if (killGroup && child.pid) {
|
|
109995
|
+
try {
|
|
109996
|
+
process.kill(-child.pid, signal);
|
|
109997
|
+
return;
|
|
109998
|
+
} catch {
|
|
109999
|
+
}
|
|
110000
|
+
}
|
|
110001
|
+
child.kill(signal);
|
|
110002
|
+
};
|
|
110003
|
+
trackChild({ child, killGroup });
|
|
109992
110004
|
let timeoutId;
|
|
109993
110005
|
let sigkillEscalatorId;
|
|
109994
110006
|
let activityCheckIntervalId;
|
|
@@ -109999,10 +110011,10 @@ async function spawn(options) {
|
|
|
109999
110011
|
if (options.timeout) {
|
|
110000
110012
|
timeoutId = setTimeout(() => {
|
|
110001
110013
|
isTimedOut = true;
|
|
110002
|
-
|
|
110014
|
+
killSelf("SIGTERM");
|
|
110003
110015
|
sigkillEscalatorId = setTimeout(() => {
|
|
110004
110016
|
if (!child.killed) {
|
|
110005
|
-
|
|
110017
|
+
killSelf("SIGKILL");
|
|
110006
110018
|
}
|
|
110007
110019
|
}, 5e3);
|
|
110008
110020
|
}, options.timeout);
|
|
@@ -110012,6 +110024,11 @@ async function spawn(options) {
|
|
|
110012
110024
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
110013
110025
|
);
|
|
110014
110026
|
activityCheckIntervalId = setInterval(() => {
|
|
110027
|
+
if (options.isPausedExternally?.()) {
|
|
110028
|
+
lastActivityTime = performance3.now();
|
|
110029
|
+
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
110030
|
+
return;
|
|
110031
|
+
}
|
|
110015
110032
|
const idleMs = performance3.now() - lastActivityTime;
|
|
110016
110033
|
log.debug(
|
|
110017
110034
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -110021,9 +110038,9 @@ async function spawn(options) {
|
|
|
110021
110038
|
killedAtIdleMs = idleMs;
|
|
110022
110039
|
const idleSec = Math.round(idleMs / 1e3);
|
|
110023
110040
|
log.info(
|
|
110024
|
-
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
|
110041
|
+
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
|
|
110025
110042
|
);
|
|
110026
|
-
|
|
110043
|
+
killSelf("SIGKILL");
|
|
110027
110044
|
clearInterval(activityCheckIntervalId);
|
|
110028
110045
|
try {
|
|
110029
110046
|
options.onActivityTimeout?.();
|
|
@@ -142532,7 +142549,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142532
142549
|
// package.json
|
|
142533
142550
|
var package_default = {
|
|
142534
142551
|
name: "pullfrog",
|
|
142535
|
-
version: "0.1.
|
|
142552
|
+
version: "0.1.1",
|
|
142536
142553
|
type: "module",
|
|
142537
142554
|
bin: {
|
|
142538
142555
|
pullfrog: "dist/cli.mjs",
|
|
@@ -147467,6 +147484,12 @@ async function runClaude(params) {
|
|
|
147467
147484
|
activityTimeout: 3e5,
|
|
147468
147485
|
onActivityTimeout: params.onActivityTimeout,
|
|
147469
147486
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147487
|
+
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147488
|
+
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
147489
|
+
// watchers, mcp transports, etc). claude itself is a node bundle so
|
|
147490
|
+
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147491
|
+
// detached + killGroup is the right default for any agent runtime.
|
|
147492
|
+
killGroup: true,
|
|
147470
147493
|
onStdout: async (chunk) => {
|
|
147471
147494
|
const text = chunk.toString();
|
|
147472
147495
|
output += text;
|
|
@@ -147826,6 +147849,9 @@ async function runOpenCode(params) {
|
|
|
147826
147849
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147827
147850
|
const pendingTaskDispatches = [];
|
|
147828
147851
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147852
|
+
function isSubagentInFlight() {
|
|
147853
|
+
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147854
|
+
}
|
|
147829
147855
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147830
147856
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147831
147857
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -148078,6 +148104,20 @@ async function runOpenCode(params) {
|
|
|
148078
148104
|
activityTimeout: 3e5,
|
|
148079
148105
|
onActivityTimeout: params.onActivityTimeout,
|
|
148080
148106
|
stdio: ["ignore", "pipe", "pipe"],
|
|
148107
|
+
// node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
|
|
148108
|
+
// the native opencode-<plat>-<arch> binary with stdio:"inherit". without
|
|
148109
|
+
// a process-group kill, SIGKILL hits only the shim, the native binary
|
|
148110
|
+
// is reparented to PID 1, holds our stdout pipe open, and `child.close`
|
|
148111
|
+
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148112
|
+
// whole tree.
|
|
148113
|
+
killGroup: true,
|
|
148114
|
+
// suspend the inner activity timer while a `task` subagent is in flight.
|
|
148115
|
+
// opencode's task tool encapsulates subagent execution in-process — the
|
|
148116
|
+
// subagent's internal events don't surface on the parent NDJSON stream,
|
|
148117
|
+
// so without this the 5min timeout would falsely fire mid-subagent.
|
|
148118
|
+
// suspend/resume is preferable to a heartbeat because there's no race
|
|
148119
|
+
// between a periodic tick and a subagent finishing between ticks.
|
|
148120
|
+
isPausedExternally: isSubagentInFlight,
|
|
148081
148121
|
onStdout: async (chunk) => {
|
|
148082
148122
|
const text = chunk.toString();
|
|
148083
148123
|
output += text;
|
|
@@ -156036,7 +156076,7 @@ async function run2() {
|
|
|
156036
156076
|
}
|
|
156037
156077
|
|
|
156038
156078
|
// cli.ts
|
|
156039
|
-
var VERSION10 = "0.1.
|
|
156079
|
+
var VERSION10 = "0.1.1";
|
|
156040
156080
|
var bin = basename2(process.argv[1] || "");
|
|
156041
156081
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156042
156082
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -109696,6 +109696,7 @@ async function spawn(options) {
|
|
|
109696
109696
|
const startTime = performance3.now();
|
|
109697
109697
|
let stdoutBuffer = "";
|
|
109698
109698
|
let stderrBuffer = "";
|
|
109699
|
+
const killGroup = options.killGroup ?? false;
|
|
109699
109700
|
return new Promise((resolve3, reject) => {
|
|
109700
109701
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
109701
109702
|
env: options.env || {
|
|
@@ -109703,9 +109704,20 @@ async function spawn(options) {
|
|
|
109703
109704
|
HOME: process.env.HOME || ""
|
|
109704
109705
|
},
|
|
109705
109706
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
|
109706
|
-
cwd: options.cwd || process.cwd()
|
|
109707
|
+
cwd: options.cwd || process.cwd(),
|
|
109708
|
+
detached: killGroup
|
|
109707
109709
|
});
|
|
109708
|
-
|
|
109710
|
+
const killSelf = (signal) => {
|
|
109711
|
+
if (killGroup && child.pid) {
|
|
109712
|
+
try {
|
|
109713
|
+
process.kill(-child.pid, signal);
|
|
109714
|
+
return;
|
|
109715
|
+
} catch {
|
|
109716
|
+
}
|
|
109717
|
+
}
|
|
109718
|
+
child.kill(signal);
|
|
109719
|
+
};
|
|
109720
|
+
trackChild({ child, killGroup });
|
|
109709
109721
|
let timeoutId;
|
|
109710
109722
|
let sigkillEscalatorId;
|
|
109711
109723
|
let activityCheckIntervalId;
|
|
@@ -109716,10 +109728,10 @@ async function spawn(options) {
|
|
|
109716
109728
|
if (options.timeout) {
|
|
109717
109729
|
timeoutId = setTimeout(() => {
|
|
109718
109730
|
isTimedOut = true;
|
|
109719
|
-
|
|
109731
|
+
killSelf("SIGTERM");
|
|
109720
109732
|
sigkillEscalatorId = setTimeout(() => {
|
|
109721
109733
|
if (!child.killed) {
|
|
109722
|
-
|
|
109734
|
+
killSelf("SIGKILL");
|
|
109723
109735
|
}
|
|
109724
109736
|
}, 5e3);
|
|
109725
109737
|
}, options.timeout);
|
|
@@ -109729,6 +109741,11 @@ async function spawn(options) {
|
|
|
109729
109741
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
109730
109742
|
);
|
|
109731
109743
|
activityCheckIntervalId = setInterval(() => {
|
|
109744
|
+
if (options.isPausedExternally?.()) {
|
|
109745
|
+
lastActivityTime = performance3.now();
|
|
109746
|
+
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
109747
|
+
return;
|
|
109748
|
+
}
|
|
109732
109749
|
const idleMs = performance3.now() - lastActivityTime;
|
|
109733
109750
|
log.debug(
|
|
109734
109751
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -109738,9 +109755,9 @@ async function spawn(options) {
|
|
|
109738
109755
|
killedAtIdleMs = idleMs;
|
|
109739
109756
|
const idleSec = Math.round(idleMs / 1e3);
|
|
109740
109757
|
log.info(
|
|
109741
|
-
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
|
109758
|
+
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
|
|
109742
109759
|
);
|
|
109743
|
-
|
|
109760
|
+
killSelf("SIGKILL");
|
|
109744
109761
|
clearInterval(activityCheckIntervalId);
|
|
109745
109762
|
try {
|
|
109746
109763
|
options.onActivityTimeout?.();
|
|
@@ -142249,7 +142266,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142249
142266
|
// package.json
|
|
142250
142267
|
var package_default = {
|
|
142251
142268
|
name: "pullfrog",
|
|
142252
|
-
version: "0.1.
|
|
142269
|
+
version: "0.1.1",
|
|
142253
142270
|
type: "module",
|
|
142254
142271
|
bin: {
|
|
142255
142272
|
pullfrog: "dist/cli.mjs",
|
|
@@ -147184,6 +147201,12 @@ async function runClaude(params) {
|
|
|
147184
147201
|
activityTimeout: 3e5,
|
|
147185
147202
|
onActivityTimeout: params.onActivityTimeout,
|
|
147186
147203
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147204
|
+
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147205
|
+
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
147206
|
+
// watchers, mcp transports, etc). claude itself is a node bundle so
|
|
147207
|
+
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147208
|
+
// detached + killGroup is the right default for any agent runtime.
|
|
147209
|
+
killGroup: true,
|
|
147187
147210
|
onStdout: async (chunk) => {
|
|
147188
147211
|
const text = chunk.toString();
|
|
147189
147212
|
output += text;
|
|
@@ -147543,6 +147566,9 @@ async function runOpenCode(params) {
|
|
|
147543
147566
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147544
147567
|
const pendingTaskDispatches = [];
|
|
147545
147568
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147569
|
+
function isSubagentInFlight() {
|
|
147570
|
+
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147571
|
+
}
|
|
147546
147572
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147547
147573
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147548
147574
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -147795,6 +147821,20 @@ async function runOpenCode(params) {
|
|
|
147795
147821
|
activityTimeout: 3e5,
|
|
147796
147822
|
onActivityTimeout: params.onActivityTimeout,
|
|
147797
147823
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147824
|
+
// node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
|
|
147825
|
+
// the native opencode-<plat>-<arch> binary with stdio:"inherit". without
|
|
147826
|
+
// a process-group kill, SIGKILL hits only the shim, the native binary
|
|
147827
|
+
// is reparented to PID 1, holds our stdout pipe open, and `child.close`
|
|
147828
|
+
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
147829
|
+
// whole tree.
|
|
147830
|
+
killGroup: true,
|
|
147831
|
+
// suspend the inner activity timer while a `task` subagent is in flight.
|
|
147832
|
+
// opencode's task tool encapsulates subagent execution in-process — the
|
|
147833
|
+
// subagent's internal events don't surface on the parent NDJSON stream,
|
|
147834
|
+
// so without this the 5min timeout would falsely fire mid-subagent.
|
|
147835
|
+
// suspend/resume is preferable to a heartbeat because there's no race
|
|
147836
|
+
// between a periodic tick and a subagent finishing between ticks.
|
|
147837
|
+
isPausedExternally: isSubagentInFlight,
|
|
147798
147838
|
onStdout: async (chunk) => {
|
|
147799
147839
|
const text = chunk.toString();
|
|
147800
147840
|
output += text;
|
|
@@ -26,6 +26,8 @@ export interface SpawnOptions {
|
|
|
26
26
|
stdio?: ("pipe" | "ignore" | "inherit")[];
|
|
27
27
|
onStdout?: (chunk: string) => void;
|
|
28
28
|
onStderr?: (chunk: string) => void;
|
|
29
|
+
killGroup?: boolean;
|
|
30
|
+
isPausedExternally?: () => boolean;
|
|
29
31
|
}
|
|
30
32
|
export interface SpawnResult {
|
|
31
33
|
stdout: string;
|