@rethinkingstudio/agent-runner 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethinkingstudio/agent-runner",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Starts your coding agent when a job lands on your Doable Agent Desk.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/Rethinking-studio/doable-agent-runner.git"
20
+ "url": "git+https://github.com/zangqilong198812/doable-agent-runner.git"
21
21
  },
22
22
  "keywords": [
23
23
  "mcp",
@@ -30,6 +30,6 @@
30
30
  },
31
31
  "homepage": "https://doable.rethinking.art",
32
32
  "bugs": {
33
- "url": "https://github.com/Rethinking-studio/doable-agent-runner/issues"
33
+ "url": "https://github.com/zangqilong198812/doable-agent-runner/issues"
34
34
  }
35
35
  }
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { writeFileSync, unlinkSync, existsSync, mkdirSync, cpSync, rmSync } from "node:fs";
13
- import { homedir, hostname, platform } from "node:os";
13
+ import { homedir, hostname, platform, userInfo } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import * as config from "./config.mjs";
@@ -63,6 +63,7 @@ function install() {
63
63
  };
64
64
 
65
65
  const path = config.save(settings);
66
+ checkAgentIsReachable(settings);
66
67
  const runtime = installRuntime();
67
68
  console.log(`Settings written to ${path}`);
68
69
  console.log(`Machine name: ${settings.name}`);
@@ -76,6 +77,33 @@ function install() {
76
77
  platform() === "darwin" ? installLaunchd(runtime) : installSystemd(runtime);
77
78
  }
78
79
 
80
+ /// launchd and systemd hand a process a minimal PATH, while agent CLIs install
81
+ /// into ~/.local/bin or a homebrew or nvm prefix. The runner starts them under a
82
+ /// login shell for exactly that reason — but if the command still cannot be
83
+ /// found, it is far better to say so now than to fail at 1am with nobody
84
+ /// watching and nothing in the log but "command not found".
85
+ function checkAgentIsReachable(settings) {
86
+ const command = settings.exec || "claude";
87
+ const binary = command.trim().split(/\s+/)[0];
88
+ const shell = (() => {
89
+ try {
90
+ return userInfo().shell || "/bin/zsh";
91
+ } catch {
92
+ return "/bin/zsh";
93
+ }
94
+ })();
95
+ const found = spawnSync(shell, ["-lc", `command -v ${binary}`], { encoding: "utf8" });
96
+ if (found.status === 0 && found.stdout.trim()) {
97
+ console.log(`Will start: ${found.stdout.trim()}`);
98
+ } else {
99
+ console.log(
100
+ `\nWARNING: could not find \`${binary}\` in your login shell.\n` +
101
+ "The runner will install, but every session it starts will fail. Either install\n" +
102
+ "that command, or re-run with --exec pointing at the right one."
103
+ );
104
+ }
105
+ }
106
+
79
107
  /// Copies the runner somewhere stable and returns the entry point to point the
80
108
  /// service at. Without this the service would reference wherever the installer
81
109
  /// happened to run from — under npx, a cache directory with no guarantees.
package/src/watch.mjs CHANGED
@@ -18,6 +18,7 @@
18
18
  // Settings come from ~/.doable/runner.json, written by `install`.
19
19
 
20
20
  import { spawn } from "node:child_process";
21
+ import { userInfo } from "node:os";
21
22
  import * as config from "./config.mjs";
22
23
 
23
24
  const settings = config.load();
@@ -51,6 +52,17 @@ if (!token) {
51
52
 
52
53
  const log = (...a) => console.log(new Date().toISOString(), ...a);
53
54
 
55
+ /// The user's own shell, so their profile — and the PATH it builds — applies.
56
+ /// Read from the password database rather than $SHELL, which launchd does not
57
+ /// set.
58
+ function loginShell() {
59
+ try {
60
+ return userInfo().shell || process.env.SHELL || "/bin/zsh";
61
+ } catch {
62
+ return process.env.SHELL || "/bin/zsh";
63
+ }
64
+ }
65
+
54
66
  /// True while a session is running. The desk allows one job per machine anyway,
55
67
  /// so starting a second session would only produce an agent that gets refused.
56
68
  let busy = false;
@@ -59,10 +71,18 @@ function runSession() {
59
71
  if (busy) return;
60
72
  busy = true;
61
73
  log("work on the desk — starting a session");
62
- // Through a shell so `--exec` can be whatever the user's agent needs. The
63
- // prompt is handed over twice on stdin and in the environment — because
64
- // CLIs disagree about which they prefer, and neither way needs quoting.
65
- const child = spawn("/bin/sh", ["-c", exec], {
74
+ // A LOGIN shell, not a plain one. launchd and systemd hand a process a
75
+ // minimal PATH /usr/bin:/bin and little else while agent CLIs install
76
+ // into ~/.local/bin, a homebrew prefix, or an nvm directory. Running under
77
+ // the user's own shell profile is what makes `claude` resolvable at all;
78
+ // without it this fails with "command not found" long after anyone is
79
+ // watching.
80
+ //
81
+ // `--exec` stays a plain shell command so it can be whatever the user's
82
+ // agent needs, and the prompt is handed over twice — on stdin and in the
83
+ // environment — because CLIs disagree about which they prefer and neither
84
+ // way needs quoting.
85
+ const child = spawn(loginShell(), ["-lc", exec], {
66
86
  cwd,
67
87
  env: { ...process.env, DOABLE_PROMPT: PROMPT },
68
88
  stdio: ["pipe", "inherit", "inherit"],
@@ -95,6 +115,10 @@ async function announce() {
95
115
  }
96
116
 
97
117
  let version = "";
118
+ /// Successful long polls are silent on purpose — one line every 25 seconds
119
+ /// would bury everything worth reading. But that makes an outage and a hang
120
+ /// look identical in the log, so recovery gets said out loud.
121
+ let failing = false;
98
122
 
99
123
  async function watch() {
100
124
  for (;;) {
@@ -113,6 +137,11 @@ async function watch() {
113
137
  continue;
114
138
  }
115
139
 
140
+ if (failing) {
141
+ log("reconnected");
142
+ failing = false;
143
+ }
144
+
116
145
  const event = await res.json();
117
146
  version = event.version;
118
147
 
@@ -124,7 +153,8 @@ async function watch() {
124
153
  else log("desk changed but nothing is queued");
125
154
  } catch (e) {
126
155
  // Sleep, laptop lid, flaky wifi — none of these deserve a crash.
127
- log("watch failed:", e.message, "— retrying in 15s");
156
+ if (!failing) log("watch failed:", e.message, "— retrying every 15s until it comes back");
157
+ failing = true;
128
158
  await sleep(15_000);
129
159
  }
130
160
  }