remote-agents 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/README.md +15 -2
- package/install.js +32 -7
- package/package.json +3 -3
- package/run.js +189 -15
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install -g remote-agents
|
|
|
14
14
|
npx remote-agents --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Supported platforms: linux x64, macOS x64/arm64, windows x64.
|
|
17
|
+
Supported platforms: linux x64/arm64, macOS x64/arm64, windows x64.
|
|
18
18
|
|
|
19
19
|
## Use as an MCP server
|
|
20
20
|
|
|
@@ -35,9 +35,22 @@ Supported platforms: linux x64, macOS x64/arm64, windows x64.
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
`remote-agents <subcommand>` forwards to the native binary (`run`, `mcp`,
|
|
38
|
-
`install`, …). See the [project README](https://github.com/
|
|
38
|
+
`install`, …). See the [project README](https://github.com/47-ronn/tunshell_mcp_agents#readme)
|
|
39
39
|
for the full documentation.
|
|
40
40
|
|
|
41
|
+
## Update checks
|
|
42
|
+
|
|
43
|
+
When started in a long-running mode (`run` / `mcp` / `hybrid`), the launcher does
|
|
44
|
+
a best-effort check against the npm registry and logs a notice if a newer
|
|
45
|
+
version is published. It is **notify-only** — the agent never self-updates, so a
|
|
46
|
+
running task is never interrupted. Update at your convenience with:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm i -g remote-agents@latest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Set `REMOTE_AGENTS_NO_UPDATE_CHECK=1` to disable the check entirely.
|
|
53
|
+
|
|
41
54
|
## License
|
|
42
55
|
|
|
43
56
|
MIT
|
package/install.js
CHANGED
|
@@ -7,12 +7,11 @@ const path = require("path");
|
|
|
7
7
|
const https = require("https");
|
|
8
8
|
const { version } = require("./package.json");
|
|
9
9
|
|
|
10
|
-
const REPO = "
|
|
10
|
+
const REPO = "47-ronn/tunshell_mcp_agents";
|
|
11
11
|
|
|
12
|
-
function target() {
|
|
13
|
-
const p = process.platform;
|
|
14
|
-
const a = process.arch;
|
|
12
|
+
function target(p = process.platform, a = process.arch) {
|
|
15
13
|
if (p === "linux" && a === "x64") return { triple: "x86_64-unknown-linux-musl", exe: "" };
|
|
14
|
+
if (p === "linux" && a === "arm64") return { triple: "aarch64-unknown-linux-musl", exe: "" };
|
|
16
15
|
if (p === "darwin" && a === "x64") return { triple: "x86_64-apple-darwin", exe: "" };
|
|
17
16
|
if (p === "darwin" && a === "arm64") return { triple: "aarch64-apple-darwin", exe: "" };
|
|
18
17
|
if (p === "win32" && a === "x64") return { triple: "x86_64-pc-windows-msvc", exe: ".exe" };
|
|
@@ -41,6 +40,27 @@ function download(url, dest, redirects = 0) {
|
|
|
41
40
|
});
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
// Retry transient download failures (flaky networks / CDN hiccups) with a small
|
|
46
|
+
// linear backoff. `doDownload`/`wait` are injectable for testing.
|
|
47
|
+
async function downloadWithRetry(url, dest, attempts = 3, doDownload = download, wait = sleep) {
|
|
48
|
+
let lastErr;
|
|
49
|
+
for (let i = 0; i < attempts; i++) {
|
|
50
|
+
try {
|
|
51
|
+
await doDownload(url, dest);
|
|
52
|
+
return;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
lastErr = e;
|
|
55
|
+
if (i < attempts - 1) {
|
|
56
|
+
console.error(`remote-agents: download attempt ${i + 1} failed (${e.message}); retrying…`);
|
|
57
|
+
await wait(500 * (i + 1));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastErr;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
async function main() {
|
|
45
65
|
// Escape hatch for CI / source installs where no matching release exists.
|
|
46
66
|
if (process.env.REMOTE_AGENTS_SKIP_DOWNLOAD) {
|
|
@@ -52,7 +72,7 @@ async function main() {
|
|
|
52
72
|
if (!t) {
|
|
53
73
|
console.error(
|
|
54
74
|
`remote-agents: no prebuilt binary for ${process.platform}/${process.arch}.\n` +
|
|
55
|
-
` Supported: linux x64, macOS x64/arm64, windows x64.\n` +
|
|
75
|
+
` Supported: linux x64/arm64, macOS x64/arm64, windows x64.\n` +
|
|
56
76
|
` Build from source: https://github.com/${REPO}`
|
|
57
77
|
);
|
|
58
78
|
process.exit(1);
|
|
@@ -66,7 +86,7 @@ async function main() {
|
|
|
66
86
|
|
|
67
87
|
console.log(`remote-agents: downloading ${asset} (v${version})…`);
|
|
68
88
|
try {
|
|
69
|
-
await
|
|
89
|
+
await downloadWithRetry(url, dest);
|
|
70
90
|
if (!t.exe) fs.chmodSync(dest, 0o755);
|
|
71
91
|
console.log(`remote-agents: installed ${dest}`);
|
|
72
92
|
} catch (e) {
|
|
@@ -77,4 +97,9 @@ async function main() {
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
|
|
100
|
+
// Only download when run as the postinstall script, not when require()'d by tests.
|
|
101
|
+
if (require.main === module) {
|
|
102
|
+
main();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { target, downloadWithRetry };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Unified MCP server for controlling fleets of remote machines through AI agents (Claude, opencode)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"ssh",
|
|
12
12
|
"devops"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://github.com/
|
|
14
|
+
"homepage": "https://github.com/47-ronn/tunshell_mcp_agents#readme",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/
|
|
17
|
+
"url": "git+https://github.com/47-ronn/tunshell_mcp_agents.git"
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "47-ron",
|
package/run.js
CHANGED
|
@@ -1,25 +1,199 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Launcher: exec the downloaded native `remote-agent` binary, forwarding args,
|
|
3
|
-
// stdin/stdout/stderr, and exit code.
|
|
3
|
+
// stdin/stdout/stderr, termination signals, and exit code.
|
|
4
|
+
// `remote-agents <cmd>` == `remote-agent <cmd>`.
|
|
5
|
+
//
|
|
6
|
+
// On startup of a long-running mode it also does a best-effort npm-registry
|
|
7
|
+
// version check and logs a notice if a newer release is published. This is
|
|
8
|
+
// NOTIFY-ONLY: the agent never self-updates (updating mid-task would interrupt
|
|
9
|
+
// it); the operator runs `npm i -g remote-agents@latest` when convenient.
|
|
4
10
|
|
|
5
11
|
const path = require("path");
|
|
6
12
|
const fs = require("fs");
|
|
7
|
-
const
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const https = require("https");
|
|
15
|
+
const { spawn } = require("child_process");
|
|
16
|
+
const { version } = require("./package.json");
|
|
8
17
|
|
|
9
|
-
const
|
|
10
|
-
|
|
18
|
+
const REGISTRY_URL = "https://registry.npmjs.org/remote-agents/latest";
|
|
19
|
+
// Only the long-running modes are worth a version check; one-shot subcommands
|
|
20
|
+
// (init/config/install/...) start and exit, so a notice there is just noise.
|
|
21
|
+
const LONG_RUNNING = new Set(["run", "mcp", "hybrid"]);
|
|
11
22
|
|
|
12
|
-
if
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
);
|
|
17
|
-
|
|
23
|
+
// True if dotted version string `latest` is strictly greater than `current`.
|
|
24
|
+
// Non-numeric / missing segments are treated as 0 (best-effort, no semver dep).
|
|
25
|
+
function isNewer(latest, current) {
|
|
26
|
+
const parse = (s) => String(s).split(".").map((n) => parseInt(n, 10) || 0);
|
|
27
|
+
const a = parse(latest);
|
|
28
|
+
const b = parse(current);
|
|
29
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
30
|
+
const x = a[i] || 0;
|
|
31
|
+
const y = b[i] || 0;
|
|
32
|
+
if (x > y) return true;
|
|
33
|
+
if (x < y) return false;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
// A human-readable upgrade notice if `latest` is newer than `current`, else null.
|
|
39
|
+
function updateNotice(current, latest) {
|
|
40
|
+
if (latest && isNewer(latest, current)) {
|
|
41
|
+
return (
|
|
42
|
+
`remote-agents: a newer version is available (${current} -> ${latest}). ` +
|
|
43
|
+
`Update with: npm i -g remote-agents@latest`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
24
47
|
}
|
|
25
|
-
|
|
48
|
+
|
|
49
|
+
// Best-effort GET of the registry's `latest` document. Resolves the parsed JSON
|
|
50
|
+
// or rejects; callers swallow errors (an update check must never break startup).
|
|
51
|
+
function httpGetJson(url, timeoutMs) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const req = https.get(
|
|
54
|
+
url,
|
|
55
|
+
{ headers: { "User-Agent": "remote-agents-updatecheck" } },
|
|
56
|
+
(res) => {
|
|
57
|
+
if (res.statusCode !== 200) {
|
|
58
|
+
res.resume();
|
|
59
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
60
|
+
}
|
|
61
|
+
let data = "";
|
|
62
|
+
res.setEncoding("utf8");
|
|
63
|
+
res.on("data", (c) => (data += c));
|
|
64
|
+
res.on("end", () => {
|
|
65
|
+
try {
|
|
66
|
+
resolve(JSON.parse(data));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
reject(e);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
req.on("error", reject);
|
|
74
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error("timeout")));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve the latest published version string, or null on any failure. `doGet`
|
|
79
|
+
// is injectable for tests.
|
|
80
|
+
function fetchLatestVersion(doGet = httpGetJson, timeoutMs = 3000) {
|
|
81
|
+
return doGet(REGISTRY_URL, timeoutMs)
|
|
82
|
+
.then((json) => (json && typeof json.version === "string" ? json.version : null))
|
|
83
|
+
.catch(() => null);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Platform data directory, mirroring Rust's `dirs::data_dir()` so the native
|
|
87
|
+
// agent reads the same cache file: Linux XDG, macOS Application Support,
|
|
88
|
+
// Windows Roaming AppData.
|
|
89
|
+
function dataDir() {
|
|
90
|
+
if (process.platform === "win32") {
|
|
91
|
+
return process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
92
|
+
}
|
|
93
|
+
if (process.platform === "darwin") {
|
|
94
|
+
return path.join(os.homedir(), "Library", "Application Support");
|
|
95
|
+
}
|
|
96
|
+
return process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Where the latest-known version is cached for the native agent to read and
|
|
100
|
+
// surface as AgentInfo.update_available.
|
|
101
|
+
function latestVersionPath() {
|
|
102
|
+
return path.join(dataDir(), "remote-agents", "latest-version");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Best-effort write of the update-available cache for the native agent to read
|
|
106
|
+
// (surfaced as AgentInfo.update_available). The launcher owns the comparison —
|
|
107
|
+
// it knows the accurate INSTALLED version (the compiled-in Cargo version can lag
|
|
108
|
+
// the npm release) — so it writes the newer version when an upgrade exists and
|
|
109
|
+
// clears the file (empty) once up to date. Never throws. `writeFile`/`mkdir`
|
|
110
|
+
// are injectable for tests.
|
|
111
|
+
function cacheUpdateAvailable(
|
|
112
|
+
latest,
|
|
113
|
+
current = version,
|
|
114
|
+
file = latestVersionPath(),
|
|
115
|
+
writeFile = fs.writeFileSync,
|
|
116
|
+
mkdir = fs.mkdirSync
|
|
117
|
+
) {
|
|
118
|
+
const body = latest && isNewer(latest, current) ? String(latest) : "";
|
|
119
|
+
try {
|
|
120
|
+
mkdir(path.dirname(file), { recursive: true });
|
|
121
|
+
writeFile(file, body);
|
|
122
|
+
} catch {
|
|
123
|
+
/* cache is best-effort; the notify-only log still fires */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fire-and-forget: log an upgrade notice for long-running modes. Never throws,
|
|
128
|
+
// never blocks the agent. Disable with REMOTE_AGENTS_NO_UPDATE_CHECK=1.
|
|
129
|
+
async function maybeNotifyUpdate(subcommand, fetchLatest = fetchLatestVersion) {
|
|
130
|
+
if (process.env.REMOTE_AGENTS_NO_UPDATE_CHECK) return;
|
|
131
|
+
if (!LONG_RUNNING.has(subcommand)) return;
|
|
132
|
+
const latest = await fetchLatest();
|
|
133
|
+
// Cache the result so the native agent can surface it as
|
|
134
|
+
// AgentInfo.update_available (visible in list_agents).
|
|
135
|
+
cacheUpdateAvailable(latest);
|
|
136
|
+
const notice = updateNotice(version, latest);
|
|
137
|
+
if (notice) console.error(notice);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main() {
|
|
141
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
142
|
+
const bin = path.join(__dirname, "bin", `remote-agent${exe}`);
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(bin)) {
|
|
145
|
+
console.error(
|
|
146
|
+
"remote-agents: native binary not found.\n" +
|
|
147
|
+
" Reinstall it with: npm install -g remote-agents (re-runs the download)."
|
|
148
|
+
);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
153
|
+
|
|
154
|
+
// Best-effort, non-blocking update check (notify-only).
|
|
155
|
+
maybeNotifyUpdate(process.argv[2]).catch(() => {});
|
|
156
|
+
|
|
157
|
+
// Forward termination signals so stopping this launcher also stops the agent
|
|
158
|
+
// (spawnSync used to orphan the native process).
|
|
159
|
+
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
160
|
+
for (const sig of SIGNALS) {
|
|
161
|
+
process.on(sig, () => {
|
|
162
|
+
if (!child.killed) {
|
|
163
|
+
try {
|
|
164
|
+
child.kill(sig);
|
|
165
|
+
} catch {
|
|
166
|
+
/* child already gone */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
child.on("error", (err) => {
|
|
173
|
+
console.error(`remote-agents: failed to launch binary: ${err.message}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.on("exit", (code, signal) => {
|
|
178
|
+
if (signal) {
|
|
179
|
+
// Mirror the child's signal in our exit status (128 + signum).
|
|
180
|
+
const num = (os.constants.signals && os.constants.signals[signal]) || 15;
|
|
181
|
+
process.exit(128 + num);
|
|
182
|
+
}
|
|
183
|
+
process.exit(code === null ? 1 : code);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Only launch when invoked directly, not when require()'d by tests.
|
|
188
|
+
if (require.main === module) {
|
|
189
|
+
main();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
isNewer,
|
|
194
|
+
updateNotice,
|
|
195
|
+
fetchLatestVersion,
|
|
196
|
+
maybeNotifyUpdate,
|
|
197
|
+
cacheUpdateAvailable,
|
|
198
|
+
latestVersionPath,
|
|
199
|
+
};
|