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 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
- trackChild({ child });
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
- child.kill("SIGTERM");
110014
+ killSelf("SIGTERM");
110003
110015
  sigkillEscalatorId = setTimeout(() => {
110004
110016
  if (!child.killed) {
110005
- child.kill("SIGKILL");
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
- child.kill("SIGKILL");
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.0",
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.0";
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
- trackChild({ child });
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
- child.kill("SIGTERM");
109731
+ killSelf("SIGTERM");
109720
109732
  sigkillEscalatorId = setTimeout(() => {
109721
109733
  if (!child.killed) {
109722
- child.kill("SIGKILL");
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
- child.kill("SIGKILL");
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.0",
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",