open-agents-ai 0.187.184 → 0.187.185
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/index.js +14 -0
- package/dist/postinstall-daemon.cjs +486 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -321794,6 +321794,17 @@ async function serveCommand(opts, config) {
|
|
|
321794
321794
|
printInfo(`Starting API server on port ${port}...`);
|
|
321795
321795
|
printInfo(`Backend: ${config.backendType} (${ollamaUrl})`);
|
|
321796
321796
|
}
|
|
321797
|
+
if (isDaemon) {
|
|
321798
|
+
try {
|
|
321799
|
+
const resp = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
321800
|
+
signal: AbortSignal.timeout(1500)
|
|
321801
|
+
});
|
|
321802
|
+
if (resp.ok) {
|
|
321803
|
+
return;
|
|
321804
|
+
}
|
|
321805
|
+
} catch {
|
|
321806
|
+
}
|
|
321807
|
+
}
|
|
321797
321808
|
try {
|
|
321798
321809
|
const server = startApiServer({ port, ollamaUrl, quiet: isQuiet });
|
|
321799
321810
|
if (isDaemon) {
|
|
@@ -321821,6 +321832,9 @@ async function serveCommand(opts, config) {
|
|
|
321821
321832
|
} catch (err) {
|
|
321822
321833
|
const msg = err instanceof Error ? err.message : String(err);
|
|
321823
321834
|
if (msg.includes("EADDRINUSE")) {
|
|
321835
|
+
if (isDaemon) {
|
|
321836
|
+
return;
|
|
321837
|
+
}
|
|
321824
321838
|
if (!isQuiet) {
|
|
321825
321839
|
printError(`Port ${port} already in use. Another OA instance may be running.`);
|
|
321826
321840
|
printInfo(`Try: oa serve --port ${port + 1}`);
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/**
|
|
4
|
+
* postinstall-daemon.cjs — system-service installer for the OA API daemon.
|
|
5
|
+
*
|
|
6
|
+
* Runs after `npm install -g open-agents-ai`. Responsibilities:
|
|
7
|
+
* 1. Clean up stale nexus daemon files (preserving prior postinstall behaviour).
|
|
8
|
+
* 2. Register a per-user system service that runs `oa serve --daemon`
|
|
9
|
+
* on port 11435, surviving reboots/logins.
|
|
10
|
+
* Linux → ~/.config/systemd/user/open-agents-daemon.service
|
|
11
|
+
* macOS → ~/Library/LaunchAgents/ai.open-agents.daemon.plist
|
|
12
|
+
* Windows → Scheduled Task "OpenAgentsDaemon" (onlogon)
|
|
13
|
+
* 3. Start (or restart) the service so the daemon is live IMMEDIATELY —
|
|
14
|
+
* no need to launch `oa` in any terminal.
|
|
15
|
+
* 4. Never exit non-zero. npm install must not fail because of a service
|
|
16
|
+
* manager quirk or a permission quirk. We warn and move on.
|
|
17
|
+
*
|
|
18
|
+
* Opt-out: set env OA_SKIP_DAEMON_INSTALL=1 before `npm i -g`.
|
|
19
|
+
*
|
|
20
|
+
* ES5-safe pure CommonJS — runs on any Node version that survived preinstall.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
var os = require("os");
|
|
26
|
+
var path = require("path");
|
|
27
|
+
var fs = require("fs");
|
|
28
|
+
var cp = require("child_process");
|
|
29
|
+
|
|
30
|
+
var HOME = os.homedir();
|
|
31
|
+
var PLATFORM = os.platform(); // "linux", "darwin", "win32"
|
|
32
|
+
var IS_WIN = PLATFORM === "win32";
|
|
33
|
+
var IS_MAC = PLATFORM === "darwin";
|
|
34
|
+
var IS_LINUX = PLATFORM === "linux";
|
|
35
|
+
|
|
36
|
+
var PORT = parseInt(process.env.OA_PORT || "11435", 10);
|
|
37
|
+
var SERVICE_LABEL = "open-agents-daemon";
|
|
38
|
+
var LAUNCHD_LABEL = "ai.open-agents.daemon";
|
|
39
|
+
var WIN_TASK_NAME = "OpenAgentsDaemon";
|
|
40
|
+
|
|
41
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function log(msg) {
|
|
44
|
+
// Use stdout so npm captures it in the install log.
|
|
45
|
+
process.stdout.write(" " + msg + "\n");
|
|
46
|
+
}
|
|
47
|
+
function warn(msg) {
|
|
48
|
+
process.stdout.write(" [daemon] " + msg + "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runQuiet(cmd, opts) {
|
|
52
|
+
try {
|
|
53
|
+
cp.execSync(cmd, Object.assign({ stdio: "pipe", timeout: 15000 }, opts || {}));
|
|
54
|
+
return true;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runCapture(cmd, opts) {
|
|
61
|
+
try {
|
|
62
|
+
return cp.execSync(cmd, Object.assign({ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 15000 }, opts || {})).trim();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasCmd(name) {
|
|
69
|
+
if (IS_WIN) return runQuiet("where " + name);
|
|
70
|
+
return runQuiet("which " + name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pre-check: is the port already serving an OA daemon?
|
|
74
|
+
function tryHealth(port, cb) {
|
|
75
|
+
var http = require("http");
|
|
76
|
+
var req = http.request(
|
|
77
|
+
{ host: "127.0.0.1", port: port, path: "/health", method: "GET", timeout: 1500 },
|
|
78
|
+
function (res) {
|
|
79
|
+
cb(res.statusCode === 200);
|
|
80
|
+
res.resume();
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
req.on("error", function () { cb(false); });
|
|
84
|
+
req.on("timeout", function () { req.destroy(); cb(false); });
|
|
85
|
+
req.end();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resolve the `oa` launcher script. On npm global install:
|
|
89
|
+
// Linux/macOS: $(npm prefix -g)/bin/oa
|
|
90
|
+
// Windows: %APPDATA%/npm/oa.cmd
|
|
91
|
+
function resolveOaBinary() {
|
|
92
|
+
// 1. If npm set npm_config_prefix, use it.
|
|
93
|
+
var prefix = process.env.npm_config_prefix || "";
|
|
94
|
+
if (!prefix) {
|
|
95
|
+
// 2. Ask npm.
|
|
96
|
+
prefix = runCapture("npm prefix -g");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var candidates = [];
|
|
100
|
+
if (prefix) {
|
|
101
|
+
if (IS_WIN) {
|
|
102
|
+
candidates.push(path.join(prefix, "oa.cmd"));
|
|
103
|
+
candidates.push(path.join(prefix, "oa"));
|
|
104
|
+
} else {
|
|
105
|
+
candidates.push(path.join(prefix, "bin", "oa"));
|
|
106
|
+
candidates.push(path.join(prefix, "bin", "open-agents"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 3. PATH fallback via which/where.
|
|
110
|
+
var found = runCapture(IS_WIN ? "where oa" : "which oa").split(/\r?\n/)[0];
|
|
111
|
+
if (found) candidates.push(found);
|
|
112
|
+
|
|
113
|
+
// 4. Relative to this script: <pkg>/dist/launcher.cjs
|
|
114
|
+
try {
|
|
115
|
+
candidates.push(path.resolve(__dirname, "launcher.cjs"));
|
|
116
|
+
} catch (e) {}
|
|
117
|
+
|
|
118
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
119
|
+
try {
|
|
120
|
+
if (candidates[i] && fs.existsSync(candidates[i])) return candidates[i];
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Find a Node binary we can use in the service ExecStart.
|
|
127
|
+
// On nvm systems this is important — /usr/bin/node is often too old.
|
|
128
|
+
function resolveNodeBinary() {
|
|
129
|
+
try {
|
|
130
|
+
if (process.execPath && fs.existsSync(process.execPath)) return process.execPath;
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
var found = runCapture(IS_WIN ? "where node" : "which node").split(/\r?\n/)[0];
|
|
133
|
+
if (found) return found;
|
|
134
|
+
return "node";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Effective user for service ownership. Handle `sudo npm i -g` by preferring
|
|
138
|
+
// SUDO_USER when running as root on Linux/macOS.
|
|
139
|
+
function effectiveUser() {
|
|
140
|
+
if (!IS_WIN && process.getuid && process.getuid() === 0) {
|
|
141
|
+
var sudoUser = process.env.SUDO_USER;
|
|
142
|
+
if (sudoUser) return sudoUser;
|
|
143
|
+
return null; // Running as bare root — skip per-user service install
|
|
144
|
+
}
|
|
145
|
+
return process.env.USER || process.env.LOGNAME || os.userInfo().username;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Nexus cleanup (preserve prior postinstall behaviour) ──────────────────
|
|
149
|
+
|
|
150
|
+
function cleanNexus() {
|
|
151
|
+
try {
|
|
152
|
+
var dirs = [
|
|
153
|
+
path.join(HOME, ".open-agents", ".oa", "nexus"),
|
|
154
|
+
path.join(process.cwd(), ".oa", "nexus"),
|
|
155
|
+
];
|
|
156
|
+
dirs.forEach(function (d) {
|
|
157
|
+
var stale = path.join(d, "nexus-daemon.mjs");
|
|
158
|
+
try {
|
|
159
|
+
if (fs.existsSync(stale)) {
|
|
160
|
+
fs.unlinkSync(stale);
|
|
161
|
+
log("Cleaned stale nexus daemon: " + stale);
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {}
|
|
164
|
+
var pidFile = path.join(d, "daemon.pid");
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(pidFile)) {
|
|
167
|
+
var n = parseInt(fs.readFileSync(pidFile, "utf8"), 10);
|
|
168
|
+
if (n > 0) {
|
|
169
|
+
try { process.kill(n, "SIGTERM"); } catch (e) {}
|
|
170
|
+
}
|
|
171
|
+
fs.unlinkSync(pidFile);
|
|
172
|
+
log("Killed old nexus daemon: PID " + n);
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {}
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Linux: systemd user unit ───────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function installSystemd(nodeBin, oaScript, user) {
|
|
182
|
+
if (!IS_LINUX) return false;
|
|
183
|
+
if (!hasCmd("systemctl")) {
|
|
184
|
+
warn("systemctl not found — skipping systemd install.");
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var userHome = user === process.env.USER ? HOME : path.join("/home", user);
|
|
189
|
+
try {
|
|
190
|
+
var pwdEntry = runCapture("getent passwd " + user);
|
|
191
|
+
if (pwdEntry) {
|
|
192
|
+
var parts = pwdEntry.split(":");
|
|
193
|
+
if (parts[5]) userHome = parts[5];
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {}
|
|
196
|
+
|
|
197
|
+
var unitDir = path.join(userHome, ".config", "systemd", "user");
|
|
198
|
+
var unitPath = path.join(unitDir, SERVICE_LABEL + ".service");
|
|
199
|
+
var logDir = path.join(userHome, ".open-agents");
|
|
200
|
+
|
|
201
|
+
try { fs.mkdirSync(unitDir, { recursive: true }); } catch (e) {}
|
|
202
|
+
try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
|
|
203
|
+
|
|
204
|
+
// The unit runs: <node> <oaScript> serve --daemon --quiet
|
|
205
|
+
// with OA_DAEMON=1 so the serve command picks the daemon path.
|
|
206
|
+
var unit = [
|
|
207
|
+
"[Unit]",
|
|
208
|
+
"Description=Open Agents API Daemon (port " + PORT + ")",
|
|
209
|
+
"Documentation=https://github.com/robit-man/open-agents",
|
|
210
|
+
"After=network-online.target",
|
|
211
|
+
"Wants=network-online.target",
|
|
212
|
+
"",
|
|
213
|
+
"[Service]",
|
|
214
|
+
"Type=simple",
|
|
215
|
+
"Environment=OA_DAEMON=1",
|
|
216
|
+
"Environment=OA_PORT=" + PORT,
|
|
217
|
+
"Environment=NODE_ENV=production",
|
|
218
|
+
"ExecStart=" + nodeBin + " " + oaScript + " serve --daemon --quiet",
|
|
219
|
+
"Restart=on-failure",
|
|
220
|
+
"RestartSec=3",
|
|
221
|
+
"StandardOutput=append:" + path.join(logDir, "daemon.log"),
|
|
222
|
+
"StandardError=append:" + path.join(logDir, "daemon.err.log"),
|
|
223
|
+
"",
|
|
224
|
+
"[Install]",
|
|
225
|
+
"WantedBy=default.target",
|
|
226
|
+
"",
|
|
227
|
+
].join("\n");
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
fs.writeFileSync(unitPath, unit, { encoding: "utf8" });
|
|
231
|
+
log("Wrote systemd user unit: " + unitPath);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
warn("Failed to write systemd unit: " + (e && e.message));
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// If we're root installing for another user, we need to wrap systemctl
|
|
238
|
+
// calls in `sudo -u USER XDG_RUNTIME_DIR=/run/user/UID systemctl --user`.
|
|
239
|
+
var asUserPrefix = "";
|
|
240
|
+
if (!IS_WIN && process.getuid && process.getuid() === 0 && user !== "root") {
|
|
241
|
+
// Try to resolve UID for the target user.
|
|
242
|
+
var uid = runCapture("id -u " + user);
|
|
243
|
+
if (uid && /^\d+$/.test(uid)) {
|
|
244
|
+
// Ensure loginctl linger so user services can run without a session.
|
|
245
|
+
runQuiet("loginctl enable-linger " + user);
|
|
246
|
+
asUserPrefix = "sudo -u " + user + " XDG_RUNTIME_DIR=/run/user/" + uid + " DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/" + uid + "/bus ";
|
|
247
|
+
}
|
|
248
|
+
} else if (!IS_WIN && process.getuid && process.getuid() !== 0) {
|
|
249
|
+
// Self-install: enable-linger ourselves if we can (best-effort; needs sudo on most distros).
|
|
250
|
+
runQuiet("loginctl enable-linger " + user);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var sysctl = asUserPrefix + "systemctl --user";
|
|
254
|
+
runQuiet(sysctl + " daemon-reload");
|
|
255
|
+
var enabled = runQuiet(sysctl + " enable " + SERVICE_LABEL + ".service");
|
|
256
|
+
var restarted = runQuiet(sysctl + " restart " + SERVICE_LABEL + ".service");
|
|
257
|
+
|
|
258
|
+
if (enabled) log("Enabled systemd unit: " + SERVICE_LABEL);
|
|
259
|
+
if (restarted) log("Started systemd unit: " + SERVICE_LABEL);
|
|
260
|
+
if (!enabled || !restarted) {
|
|
261
|
+
warn("Could not enable/start the unit via systemctl --user. The file is in place; run manually:");
|
|
262
|
+
warn(" systemctl --user daemon-reload");
|
|
263
|
+
warn(" systemctl --user enable --now " + SERVICE_LABEL + ".service");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── macOS: launchd user agent ──────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function installLaunchd(nodeBin, oaScript, user) {
|
|
272
|
+
if (!IS_MAC) return false;
|
|
273
|
+
|
|
274
|
+
var userHome = HOME;
|
|
275
|
+
if (user && user !== os.userInfo().username) {
|
|
276
|
+
try {
|
|
277
|
+
var home = runCapture("dscl . -read /Users/" + user + " NFSHomeDirectory").split(" ").pop();
|
|
278
|
+
if (home) userHome = home;
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var agentDir = path.join(userHome, "Library", "LaunchAgents");
|
|
283
|
+
var plistPath = path.join(agentDir, LAUNCHD_LABEL + ".plist");
|
|
284
|
+
var logDir = path.join(userHome, ".open-agents");
|
|
285
|
+
|
|
286
|
+
try { fs.mkdirSync(agentDir, { recursive: true }); } catch (e) {}
|
|
287
|
+
try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
|
|
288
|
+
|
|
289
|
+
var plist = [
|
|
290
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
291
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
292
|
+
'<plist version="1.0">',
|
|
293
|
+
'<dict>',
|
|
294
|
+
' <key>Label</key><string>' + LAUNCHD_LABEL + '</string>',
|
|
295
|
+
' <key>ProgramArguments</key>',
|
|
296
|
+
' <array>',
|
|
297
|
+
' <string>' + nodeBin + '</string>',
|
|
298
|
+
' <string>' + oaScript + '</string>',
|
|
299
|
+
' <string>serve</string>',
|
|
300
|
+
' <string>--daemon</string>',
|
|
301
|
+
' <string>--quiet</string>',
|
|
302
|
+
' </array>',
|
|
303
|
+
' <key>EnvironmentVariables</key>',
|
|
304
|
+
' <dict>',
|
|
305
|
+
' <key>OA_DAEMON</key><string>1</string>',
|
|
306
|
+
' <key>OA_PORT</key><string>' + PORT + '</string>',
|
|
307
|
+
' <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>',
|
|
308
|
+
' </dict>',
|
|
309
|
+
' <key>RunAtLoad</key><true/>',
|
|
310
|
+
' <key>KeepAlive</key><true/>',
|
|
311
|
+
' <key>StandardOutPath</key><string>' + path.join(logDir, "daemon.log") + '</string>',
|
|
312
|
+
' <key>StandardErrorPath</key><string>' + path.join(logDir, "daemon.err.log") + '</string>',
|
|
313
|
+
'</dict>',
|
|
314
|
+
'</plist>',
|
|
315
|
+
'',
|
|
316
|
+
].join("\n");
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
fs.writeFileSync(plistPath, plist, { encoding: "utf8" });
|
|
320
|
+
log("Wrote launchd plist: " + plistPath);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
warn("Failed to write launchd plist: " + (e && e.message));
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Unload then load (idempotent). bootout/bootstrap prefer gui/<uid> on newer macOS.
|
|
327
|
+
var uid = runCapture("id -u " + user);
|
|
328
|
+
if (uid && /^\d+$/.test(uid)) {
|
|
329
|
+
runQuiet("launchctl bootout gui/" + uid + "/" + LAUNCHD_LABEL);
|
|
330
|
+
var booted = runQuiet("launchctl bootstrap gui/" + uid + " " + plistPath);
|
|
331
|
+
if (booted) log("Loaded launchd agent: " + LAUNCHD_LABEL);
|
|
332
|
+
runQuiet("launchctl kickstart -k gui/" + uid + "/" + LAUNCHD_LABEL);
|
|
333
|
+
} else {
|
|
334
|
+
runQuiet("launchctl unload " + plistPath);
|
|
335
|
+
runQuiet("launchctl load -w " + plistPath);
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Windows: Scheduled Task ────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function installWindows(nodeBin, oaScript) {
|
|
343
|
+
if (!IS_WIN) return false;
|
|
344
|
+
if (!hasCmd("schtasks")) {
|
|
345
|
+
warn("schtasks not found — skipping Windows scheduled task install.");
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Build the task command — quote the paths.
|
|
350
|
+
var trCmd = '"' + nodeBin + '" "' + oaScript + '" serve --daemon --quiet';
|
|
351
|
+
|
|
352
|
+
// Delete any existing task first (idempotent).
|
|
353
|
+
runQuiet('schtasks /Delete /TN "' + WIN_TASK_NAME + '" /F');
|
|
354
|
+
|
|
355
|
+
var created = runQuiet(
|
|
356
|
+
'schtasks /Create /F /SC ONLOGON /RL HIGHEST /TN "' + WIN_TASK_NAME + '" /TR ' + JSON.stringify(trCmd)
|
|
357
|
+
);
|
|
358
|
+
if (!created) {
|
|
359
|
+
warn("Failed to create scheduled task. Try running `oa daemon install` from an elevated shell.");
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
log("Created scheduled task: " + WIN_TASK_NAME);
|
|
363
|
+
// Start it immediately.
|
|
364
|
+
runQuiet('schtasks /Run /TN "' + WIN_TASK_NAME + '"');
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─── Fallback: spawn detached now (no persistence) ─────────────────────────
|
|
369
|
+
|
|
370
|
+
function spawnDetached(nodeBin, oaScript) {
|
|
371
|
+
try {
|
|
372
|
+
var child = cp.spawn(nodeBin, [oaScript, "serve", "--daemon", "--quiet"], {
|
|
373
|
+
detached: true,
|
|
374
|
+
stdio: "ignore",
|
|
375
|
+
env: Object.assign({}, process.env, { OA_DAEMON: "1", OA_PORT: String(PORT) }),
|
|
376
|
+
});
|
|
377
|
+
child.unref();
|
|
378
|
+
if (child.pid) {
|
|
379
|
+
log("Spawned detached daemon (PID " + child.pid + "). It will NOT survive reboot — install a service manager to persist.");
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
warn("Failed to spawn detached daemon: " + (e && e.message));
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Wait for /health ───────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
function waitForHealth(timeoutMs, cb) {
|
|
391
|
+
var start = Date.now();
|
|
392
|
+
function tick() {
|
|
393
|
+
tryHealth(PORT, function (ok) {
|
|
394
|
+
if (ok) return cb(true);
|
|
395
|
+
if (Date.now() - start > timeoutMs) return cb(false);
|
|
396
|
+
setTimeout(tick, 500);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
tick();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
function main() {
|
|
405
|
+
// Always do the nexus cleanup first, regardless of opt-out.
|
|
406
|
+
cleanNexus();
|
|
407
|
+
|
|
408
|
+
if (process.env.OA_SKIP_DAEMON_INSTALL === "1") {
|
|
409
|
+
log("OA_SKIP_DAEMON_INSTALL=1 — skipping daemon service install.");
|
|
410
|
+
return safeExit(0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fast path: daemon already answering /health on the target port.
|
|
414
|
+
tryHealth(PORT, function (alreadyUp) {
|
|
415
|
+
var user = effectiveUser();
|
|
416
|
+
var nodeBin = resolveNodeBinary();
|
|
417
|
+
var oaScript = resolveOaBinary();
|
|
418
|
+
|
|
419
|
+
if (!oaScript) {
|
|
420
|
+
warn("Could not resolve `oa` launcher — daemon install skipped. After install completes, run `oa daemon install` manually.");
|
|
421
|
+
return safeExit(0);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (alreadyUp) {
|
|
425
|
+
log("OA daemon already responding on port " + PORT + " — reinstalling service definition so it picks up the new build.");
|
|
426
|
+
// Fall through to (re)install service files; the serve command's
|
|
427
|
+
// daemon-mode /health pre-check will no-op if the existing daemon
|
|
428
|
+
// is still healthy.
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
log("Installing OA API daemon service for port " + PORT + " ...");
|
|
432
|
+
log(" node: " + nodeBin);
|
|
433
|
+
log(" oa script: " + oaScript);
|
|
434
|
+
log(" user: " + (user || "(unknown)"));
|
|
435
|
+
|
|
436
|
+
if (!user) {
|
|
437
|
+
warn("Running as bare root with no SUDO_USER — skipping per-user service install.");
|
|
438
|
+
warn("Re-run as your user, or run: OA_SKIP_DAEMON_INSTALL=0 npm i -g open-agents-ai");
|
|
439
|
+
// Still spawn detached so the port is live right now.
|
|
440
|
+
spawnDetached(nodeBin, oaScript);
|
|
441
|
+
return safeExit(0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
var ok = false;
|
|
445
|
+
if (IS_LINUX) {
|
|
446
|
+
ok = installSystemd(nodeBin, oaScript, user);
|
|
447
|
+
} else if (IS_MAC) {
|
|
448
|
+
ok = installLaunchd(nodeBin, oaScript, user);
|
|
449
|
+
} else if (IS_WIN) {
|
|
450
|
+
ok = installWindows(nodeBin, oaScript);
|
|
451
|
+
} else {
|
|
452
|
+
warn("Unsupported platform: " + PLATFORM + " — falling back to detached spawn.");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!ok) {
|
|
456
|
+
// Fallback: spawn a detached child so at least the port is live NOW.
|
|
457
|
+
spawnDetached(nodeBin, oaScript);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Wait up to 15s for /health to come up, but don't fail npm install.
|
|
461
|
+
waitForHealth(15000, function (healthy) {
|
|
462
|
+
if (healthy) {
|
|
463
|
+
log("OA API daemon is live: http://127.0.0.1:" + PORT + "/health");
|
|
464
|
+
} else {
|
|
465
|
+
warn("OA API daemon did not answer /health within 15s. Check logs:");
|
|
466
|
+
if (IS_LINUX) warn(" systemctl --user status " + SERVICE_LABEL);
|
|
467
|
+
if (IS_MAC) warn(" launchctl print gui/$(id -u)/" + LAUNCHD_LABEL);
|
|
468
|
+
if (IS_WIN) warn(" schtasks /Query /TN " + WIN_TASK_NAME + " /V /FO LIST");
|
|
469
|
+
}
|
|
470
|
+
safeExit(0);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function safeExit(code) {
|
|
476
|
+
// Never exit non-zero from postinstall — it breaks `npm i -g`.
|
|
477
|
+
process.exit(typeof code === "number" ? 0 : 0);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Guard against any unhandled throw — postinstall must not crash npm.
|
|
481
|
+
try {
|
|
482
|
+
main();
|
|
483
|
+
} catch (e) {
|
|
484
|
+
warn("postinstall crashed (non-fatal): " + (e && e.message));
|
|
485
|
+
safeExit(0);
|
|
486
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-agents-ai",
|
|
3
|
-
"version": "0.187.
|
|
3
|
+
"version": "0.187.185",
|
|
4
4
|
"description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"license": "CC-BY-NC-4.0",
|
|
73
73
|
"scripts": {
|
|
74
74
|
"preinstall": "node dist/preinstall.cjs",
|
|
75
|
-
"postinstall": "node -
|
|
75
|
+
"postinstall": "node dist/postinstall-daemon.cjs"
|
|
76
76
|
},
|
|
77
77
|
"engines": {
|
|
78
78
|
"node": ">=22.0.0"
|