omnius 1.0.0

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.
Files changed (60) hide show
  1. package/README.md +4959 -0
  2. package/dist/index.d.ts +6 -0
  3. package/dist/index.js +630665 -0
  4. package/dist/launcher.cjs +78 -0
  5. package/dist/postinstall-daemon.cjs +776 -0
  6. package/dist/preinstall.cjs +92 -0
  7. package/dist/scripts/autoresearch-prepare.py +459 -0
  8. package/dist/scripts/autoresearch-train.py +661 -0
  9. package/dist/scripts/crawlee-scraper.py +358 -0
  10. package/dist/scripts/live-nemotron.py +478 -0
  11. package/dist/scripts/live-whisper.py +242 -0
  12. package/dist/scripts/ocr-advanced.py +571 -0
  13. package/dist/scripts/start-moondream.py +112 -0
  14. package/dist/scripts/tor/UPSTREAM-README.md +148 -0
  15. package/dist/scripts/tor/destroy_tor.sh +29 -0
  16. package/dist/scripts/tor/tor_setup.sh +163 -0
  17. package/dist/scripts/transcribe-file.py +63 -0
  18. package/dist/scripts/web_scrape.py +1295 -0
  19. package/npm-shrinkwrap.json +7412 -0
  20. package/package.json +142 -0
  21. package/prompts/agentic/system-large.md +569 -0
  22. package/prompts/agentic/system-medium.md +211 -0
  23. package/prompts/agentic/system-small.md +114 -0
  24. package/prompts/compaction/context-compaction.md +44 -0
  25. package/prompts/personality/level-1-minimal.md +3 -0
  26. package/prompts/personality/level-2-concise.md +3 -0
  27. package/prompts/personality/level-4-explanatory.md +3 -0
  28. package/prompts/personality/level-5-thorough.md +3 -0
  29. package/prompts/personality/level-autist.md +3 -0
  30. package/prompts/personality/level-stark.md +3 -0
  31. package/prompts/runners/dispatcher.md +24 -0
  32. package/prompts/runners/editor.md +44 -0
  33. package/prompts/runners/evaluator.md +30 -0
  34. package/prompts/runners/merge-summary.md +9 -0
  35. package/prompts/runners/normalizer.md +23 -0
  36. package/prompts/runners/planner.md +33 -0
  37. package/prompts/runners/scout.md +39 -0
  38. package/prompts/runners/verifier.md +36 -0
  39. package/prompts/skill-builder/seed-analysis.md +30 -0
  40. package/prompts/skill-builder/skill-expansion.md +76 -0
  41. package/prompts/skill-builder/skill-validation.md +31 -0
  42. package/prompts/templates/analysis.md +14 -0
  43. package/prompts/templates/code-review.md +16 -0
  44. package/prompts/templates/code.md +13 -0
  45. package/prompts/templates/document.md +13 -0
  46. package/prompts/templates/error-diagnosis.md +14 -0
  47. package/prompts/templates/general.md +9 -0
  48. package/prompts/templates/plan.md +15 -0
  49. package/prompts/templates/system.md +16 -0
  50. package/prompts/tui/dmn-gather.md +128 -0
  51. package/prompts/tui/dream-consolidate.md +48 -0
  52. package/prompts/tui/dream-lucid-eval.md +17 -0
  53. package/prompts/tui/dream-lucid-implement.md +14 -0
  54. package/prompts/tui/dream-stages.md +19 -0
  55. package/prompts/tui/emotion-behavioral.md +2 -0
  56. package/prompts/tui/emotion-center.md +12 -0
  57. package/voices/personaplex/OverBarn.pt +0 -0
  58. package/voices/personaplex/clone-voice.py +384 -0
  59. package/voices/personaplex/dequant-loader.py +174 -0
  60. package/voices/personaplex/quantize-weights.py +167 -0
@@ -0,0 +1,776 @@
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.
89
+ //
90
+ // PRIORITY ORDER (most stable first):
91
+ // 1. Sibling launcher.cjs — guaranteed to live next to this postinstall,
92
+ // survives across upgrades because it's part of the package itself.
93
+ // Avoids the npx cache trap and the fnm shim trap.
94
+ // 2. npm-global bin (if npm_config_prefix is set) — stable across reboots.
95
+ // 3. PATH-resolved oa — works in dev installs but can resolve to ephemeral
96
+ // shims (npx cache, fnm multishell symlinks).
97
+ function resolveOaBinary() {
98
+ var candidates = [];
99
+
100
+ // 1. Self-relative — IDEAL: the launcher ships in the same directory as
101
+ // this postinstall script, so as long as the package is installed, it
102
+ // exists. This path is what should land in the systemd ExecStart.
103
+ try {
104
+ candidates.push(path.resolve(__dirname, "launcher.cjs"));
105
+ } catch (e) {}
106
+
107
+ // 2. npm prefix bin
108
+ var prefix = process.env.npm_config_prefix || "";
109
+ if (!prefix) prefix = runCapture("npm prefix -g");
110
+ if (prefix) {
111
+ if (IS_WIN) {
112
+ candidates.push(path.join(prefix, "oa.cmd"));
113
+ candidates.push(path.join(prefix, "oa"));
114
+ } else {
115
+ candidates.push(path.join(prefix, "bin", "oa"));
116
+ candidates.push(path.join(prefix, "bin", "open-agents"));
117
+ }
118
+ }
119
+ // 3. PATH fallback (last resort — can resolve to npx cache or fnm shim).
120
+ var found = runCapture(IS_WIN ? "where oa" : "which oa").split(/\r?\n/)[0];
121
+ if (found && !/_npx|fnm_multishells/.test(found)) candidates.push(found);
122
+
123
+ for (var i = 0; i < candidates.length; i++) {
124
+ try {
125
+ if (candidates[i] && fs.existsSync(candidates[i])) return candidates[i];
126
+ } catch (e) {}
127
+ }
128
+ return null;
129
+ }
130
+
131
+ // Find a Node binary we can use in the service ExecStart.
132
+ // On nvm systems this is important — /usr/bin/node is often too old.
133
+ function resolveNodeBinary() {
134
+ try {
135
+ if (process.execPath && fs.existsSync(process.execPath)) return process.execPath;
136
+ } catch (e) {}
137
+ var found = runCapture(IS_WIN ? "where node" : "which node").split(/\r?\n/)[0];
138
+ if (found) return found;
139
+ return "node";
140
+ }
141
+
142
+ // Effective user for service ownership. Handle `sudo npm i -g` by preferring
143
+ // SUDO_USER when running as root on Linux/macOS.
144
+ function effectiveUser() {
145
+ if (!IS_WIN && process.getuid && process.getuid() === 0) {
146
+ var sudoUser = process.env.SUDO_USER;
147
+ if (sudoUser) return sudoUser;
148
+ return null; // Running as bare root — skip per-user service install
149
+ }
150
+ return process.env.USER || process.env.LOGNAME || os.userInfo().username;
151
+ }
152
+
153
+ // ─── Force-kill port holder (regardless of how it was launched) ────────────
154
+ //
155
+ // Critical for upgrade correctness: when an OLD daemon (started before this
156
+ // install) is running, it holds port 11435 with stale in-memory code. The
157
+ // systemctl/launchctl `restart` calls below CANNOT reach it because the
158
+ // service manager doesn't own that process — it was started detached by
159
+ // `oa serve --daemon` from a previous TUI session.
160
+ //
161
+ // Result without this kill: postinstall tries to start a NEW systemd
162
+ // daemon, port-bind fails, old daemon stays alive serving stale code,
163
+ // and the user's runs continue with broken patches.
164
+ //
165
+ // Strategy (matches packages/cli/src/daemon.ts:forceKillDaemon):
166
+ // 1. SIGTERM via known PID files (~/.open-agents/daemon.pid + .oa/nexus/daemon.pid)
167
+ // 2. lsof / fuser port probe — find ANY other holder
168
+ // 3. 2s graceful grace, then SIGKILL stragglers
169
+ // 4. Poll /health up to 5s for confirmation
170
+ //
171
+ // Skips when OA_DISABLE_FORCE_KILL_DAEMON=1 (escape valve).
172
+ function forceKillPortHolder(port, cb) {
173
+ if (process.env.OA_DISABLE_FORCE_KILL_DAEMON === "1") {
174
+ log("OA_DISABLE_FORCE_KILL_DAEMON=1 — skipping port-holder kill (upgrades may not pick up new code).");
175
+ return cb(0);
176
+ }
177
+
178
+ var killed = 0;
179
+
180
+ // Step 1: SIGTERM via PID files (graceful)
181
+ var pidFiles = [
182
+ path.join(HOME, ".open-agents", "daemon.pid"),
183
+ path.join(process.cwd(), ".oa", "nexus", "daemon.pid"),
184
+ ];
185
+ pidFiles.forEach(function (pidFile) {
186
+ try {
187
+ if (!fs.existsSync(pidFile)) return;
188
+ var n = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
189
+ if (!n || n <= 0) return;
190
+ try { process.kill(n, "SIGTERM"); killed++; log("SIGTERM old daemon (pid " + n + ", from " + pidFile + ")"); } catch (e) { /* dead */ }
191
+ } catch (e) { /* */ }
192
+ });
193
+
194
+ // Step 2: port probe — lsof/fuser to find ANY pid holding the port
195
+ try {
196
+ var out = "";
197
+ try {
198
+ out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
199
+ encoding: "utf8", timeout: 3000,
200
+ }).trim();
201
+ } catch (e) { /* tools unavailable */ }
202
+ if (out) {
203
+ var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
204
+ return Number.isFinite(n) && n > 0 && n !== process.pid;
205
+ });
206
+ pids.forEach(function (otherPid) {
207
+ try { process.kill(otherPid, "SIGTERM"); killed++; log("SIGTERM port-holder (pid " + otherPid + ")"); } catch (e) { /* */ }
208
+ });
209
+ }
210
+ } catch (e) { /* */ }
211
+
212
+ if (killed === 0) {
213
+ // Nothing to kill — port was already free.
214
+ return cb(0);
215
+ }
216
+
217
+ // Step 3: 2s grace, then SIGKILL stragglers
218
+ setTimeout(function () {
219
+ try {
220
+ var out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
221
+ encoding: "utf8", timeout: 3000,
222
+ }).trim();
223
+ if (out) {
224
+ var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
225
+ return Number.isFinite(n) && n > 0 && n !== process.pid;
226
+ });
227
+ pids.forEach(function (otherPid) {
228
+ try { process.kill(otherPid, "SIGKILL"); log("SIGKILL straggler (pid " + otherPid + ")"); } catch (e) { /* */ }
229
+ });
230
+ }
231
+ } catch (e) { /* */ }
232
+
233
+ // Step 4: poll until port is free (up to 5s) — required so the next
234
+ // service-manager restart binds without conflict.
235
+ var pollStart = Date.now();
236
+ function pollFree() {
237
+ if (Date.now() - pollStart > 5000) return cb(killed);
238
+ tryHealth(port, function (alive) {
239
+ if (!alive) return cb(killed);
240
+ setTimeout(pollFree, 200);
241
+ });
242
+ }
243
+ pollFree();
244
+
245
+ // Clean up PID files now that the processes are dead
246
+ pidFiles.forEach(function (pidFile) {
247
+ try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch (e) { /* */ }
248
+ });
249
+ }, 2000);
250
+ }
251
+
252
+ // ─── Nexus cleanup (preserve prior postinstall behaviour) ──────────────────
253
+
254
+ function cleanNexus() {
255
+ try {
256
+ var dirs = [
257
+ path.join(HOME, ".open-agents", ".oa", "nexus"),
258
+ path.join(process.cwd(), ".oa", "nexus"),
259
+ ];
260
+ dirs.forEach(function (d) {
261
+ var stale = path.join(d, "nexus-daemon.mjs");
262
+ try {
263
+ if (fs.existsSync(stale)) {
264
+ fs.unlinkSync(stale);
265
+ log("Cleaned stale nexus daemon: " + stale);
266
+ }
267
+ } catch (e) {}
268
+ var pidFile = path.join(d, "daemon.pid");
269
+ try {
270
+ if (fs.existsSync(pidFile)) {
271
+ var n = parseInt(fs.readFileSync(pidFile, "utf8"), 10);
272
+ if (n > 0) {
273
+ try { process.kill(n, "SIGTERM"); } catch (e) {}
274
+ }
275
+ fs.unlinkSync(pidFile);
276
+ log("Killed old nexus daemon: PID " + n);
277
+ }
278
+ } catch (e) {}
279
+ });
280
+ } catch (e) {}
281
+ }
282
+
283
+ // ─── Linux: systemd user unit ───────────────────────────────────────────────
284
+
285
+ function installSystemd(nodeBin, oaScript, user) {
286
+ if (!IS_LINUX) return false;
287
+ if (!hasCmd("systemctl")) {
288
+ warn("systemctl not found — skipping systemd install.");
289
+ return false;
290
+ }
291
+
292
+ var userHome = user === process.env.USER ? HOME : path.join("/home", user);
293
+ try {
294
+ var pwdEntry = runCapture("getent passwd " + user);
295
+ if (pwdEntry) {
296
+ var parts = pwdEntry.split(":");
297
+ if (parts[5]) userHome = parts[5];
298
+ }
299
+ } catch (e) {}
300
+
301
+ var unitDir = path.join(userHome, ".config", "systemd", "user");
302
+ var unitPath = path.join(unitDir, SERVICE_LABEL + ".service");
303
+ var logDir = path.join(userHome, ".open-agents");
304
+
305
+ try { fs.mkdirSync(unitDir, { recursive: true }); } catch (e) {}
306
+ try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
307
+
308
+ // The unit runs: <node> <oaScript> serve --daemon --quiet
309
+ // with OA_DAEMON=1 so the serve command picks the daemon path.
310
+ var unit = [
311
+ "[Unit]",
312
+ "Description=Open Agents API Daemon (port " + PORT + ")",
313
+ "Documentation=https://github.com/robit-man/open-agents",
314
+ "After=network-online.target",
315
+ "Wants=network-online.target",
316
+ "",
317
+ "[Service]",
318
+ "Type=simple",
319
+ "Environment=OA_DAEMON=1",
320
+ "Environment=OA_PORT=" + PORT,
321
+ "Environment=NODE_ENV=production",
322
+ "ExecStart=" + nodeBin + " " + oaScript + " serve --daemon --quiet",
323
+ // Restart=always (was on-failure) — also relaunch on clean exit.
324
+ // Some upgrade flows trigger process.exit(0) (e.g. /update reload,
325
+ // SIGTERM during npm install), which Restart=on-failure ignores.
326
+ // Pair with StartLimitIntervalSec to prevent thrash loops.
327
+ "Restart=always",
328
+ "RestartSec=3",
329
+ "StartLimitIntervalSec=30",
330
+ "StartLimitBurst=10",
331
+ "StandardOutput=append:" + path.join(logDir, "daemon.log"),
332
+ "StandardError=append:" + path.join(logDir, "daemon.err.log"),
333
+ "",
334
+ "[Install]",
335
+ "WantedBy=default.target",
336
+ "",
337
+ ].join("\n");
338
+
339
+ try {
340
+ fs.writeFileSync(unitPath, unit, { encoding: "utf8" });
341
+ log("Wrote systemd user unit: " + unitPath);
342
+ } catch (e) {
343
+ warn("Failed to write systemd unit: " + (e && e.message));
344
+ return false;
345
+ }
346
+
347
+ // If we're root installing for another user, we need to wrap systemctl
348
+ // calls in `sudo -u USER XDG_RUNTIME_DIR=/run/user/UID systemctl --user`.
349
+ var asUserPrefix = "";
350
+ if (!IS_WIN && process.getuid && process.getuid() === 0 && user !== "root") {
351
+ // Try to resolve UID for the target user.
352
+ var uid = runCapture("id -u " + user);
353
+ if (uid && /^\d+$/.test(uid)) {
354
+ // Ensure loginctl linger so user services can run without a session.
355
+ runQuiet("loginctl enable-linger " + user);
356
+ asUserPrefix = "sudo -u " + user + " XDG_RUNTIME_DIR=/run/user/" + uid + " DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/" + uid + "/bus ";
357
+ }
358
+ } else if (!IS_WIN && process.getuid && process.getuid() !== 0) {
359
+ // Self-install: enable-linger ourselves if we can (best-effort; needs sudo on most distros).
360
+ runQuiet("loginctl enable-linger " + user);
361
+ }
362
+
363
+ var sysctl = asUserPrefix + "systemctl --user";
364
+ runQuiet(sysctl + " daemon-reload");
365
+ var enabled = runQuiet(sysctl + " enable " + SERVICE_LABEL + ".service");
366
+ var restarted = runQuiet(sysctl + " restart " + SERVICE_LABEL + ".service");
367
+
368
+ if (enabled) log("Enabled systemd unit: " + SERVICE_LABEL);
369
+ if (restarted) log("Started systemd unit: " + SERVICE_LABEL);
370
+ if (!enabled || !restarted) {
371
+ warn("Could not enable/start the unit via systemctl --user. The file is in place; run manually:");
372
+ warn(" systemctl --user daemon-reload");
373
+ warn(" systemctl --user enable --now " + SERVICE_LABEL + ".service");
374
+ }
375
+
376
+ return true;
377
+ }
378
+
379
+ // ─── macOS: launchd user agent ──────────────────────────────────────────────
380
+
381
+ function installLaunchd(nodeBin, oaScript, user) {
382
+ if (!IS_MAC) return false;
383
+
384
+ var userHome = HOME;
385
+ if (user && user !== os.userInfo().username) {
386
+ try {
387
+ var home = runCapture("dscl . -read /Users/" + user + " NFSHomeDirectory").split(" ").pop();
388
+ if (home) userHome = home;
389
+ } catch (e) {}
390
+ }
391
+
392
+ var agentDir = path.join(userHome, "Library", "LaunchAgents");
393
+ var plistPath = path.join(agentDir, LAUNCHD_LABEL + ".plist");
394
+ var logDir = path.join(userHome, ".open-agents");
395
+
396
+ try { fs.mkdirSync(agentDir, { recursive: true }); } catch (e) {}
397
+ try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
398
+
399
+ var plist = [
400
+ '<?xml version="1.0" encoding="UTF-8"?>',
401
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
402
+ '<plist version="1.0">',
403
+ '<dict>',
404
+ ' <key>Label</key><string>' + LAUNCHD_LABEL + '</string>',
405
+ ' <key>ProgramArguments</key>',
406
+ ' <array>',
407
+ ' <string>' + nodeBin + '</string>',
408
+ ' <string>' + oaScript + '</string>',
409
+ ' <string>serve</string>',
410
+ ' <string>--daemon</string>',
411
+ ' <string>--quiet</string>',
412
+ ' </array>',
413
+ ' <key>EnvironmentVariables</key>',
414
+ ' <dict>',
415
+ ' <key>OA_DAEMON</key><string>1</string>',
416
+ ' <key>OA_PORT</key><string>' + PORT + '</string>',
417
+ ' <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>',
418
+ ' </dict>',
419
+ ' <key>RunAtLoad</key><true/>',
420
+ ' <key>KeepAlive</key><true/>',
421
+ ' <key>StandardOutPath</key><string>' + path.join(logDir, "daemon.log") + '</string>',
422
+ ' <key>StandardErrorPath</key><string>' + path.join(logDir, "daemon.err.log") + '</string>',
423
+ '</dict>',
424
+ '</plist>',
425
+ '',
426
+ ].join("\n");
427
+
428
+ try {
429
+ fs.writeFileSync(plistPath, plist, { encoding: "utf8" });
430
+ log("Wrote launchd plist: " + plistPath);
431
+ } catch (e) {
432
+ warn("Failed to write launchd plist: " + (e && e.message));
433
+ return false;
434
+ }
435
+
436
+ // Unload then load (idempotent). bootout/bootstrap prefer gui/<uid> on newer macOS.
437
+ var uid = runCapture("id -u " + user);
438
+ if (uid && /^\d+$/.test(uid)) {
439
+ runQuiet("launchctl bootout gui/" + uid + "/" + LAUNCHD_LABEL);
440
+ var booted = runQuiet("launchctl bootstrap gui/" + uid + " " + plistPath);
441
+ if (booted) log("Loaded launchd agent: " + LAUNCHD_LABEL);
442
+ runQuiet("launchctl kickstart -k gui/" + uid + "/" + LAUNCHD_LABEL);
443
+ } else {
444
+ runQuiet("launchctl unload " + plistPath);
445
+ runQuiet("launchctl load -w " + plistPath);
446
+ }
447
+ return true;
448
+ }
449
+
450
+ // ─── Windows: Scheduled Task ────────────────────────────────────────────────
451
+
452
+ function installWindows(nodeBin, oaScript) {
453
+ if (!IS_WIN) return false;
454
+ if (!hasCmd("schtasks")) {
455
+ warn("schtasks not found — skipping Windows scheduled task install.");
456
+ return false;
457
+ }
458
+
459
+ // Build the task command — quote the paths.
460
+ var trCmd = '"' + nodeBin + '" "' + oaScript + '" serve --daemon --quiet';
461
+
462
+ // Delete any existing task first (idempotent).
463
+ runQuiet('schtasks /Delete /TN "' + WIN_TASK_NAME + '" /F');
464
+
465
+ var created = runQuiet(
466
+ 'schtasks /Create /F /SC ONLOGON /RL HIGHEST /TN "' + WIN_TASK_NAME + '" /TR ' + JSON.stringify(trCmd)
467
+ );
468
+ if (!created) {
469
+ warn("Failed to create scheduled task. Try running `oa daemon install` from an elevated shell.");
470
+ return false;
471
+ }
472
+ log("Created scheduled task: " + WIN_TASK_NAME);
473
+ // Start it immediately.
474
+ runQuiet('schtasks /Run /TN "' + WIN_TASK_NAME + '"');
475
+ return true;
476
+ }
477
+
478
+ // ─── Fallback: spawn detached now (no persistence) ─────────────────────────
479
+
480
+ function spawnDetached(nodeBin, oaScript) {
481
+ try {
482
+ var child = cp.spawn(nodeBin, [oaScript, "serve", "--daemon", "--quiet"], {
483
+ detached: true,
484
+ stdio: "ignore",
485
+ env: Object.assign({}, process.env, { OA_DAEMON: "1", OA_PORT: String(PORT) }),
486
+ });
487
+ child.unref();
488
+ if (child.pid) {
489
+ log("Spawned detached daemon (PID " + child.pid + "). It will NOT survive reboot — install a service manager to persist.");
490
+ return true;
491
+ }
492
+ } catch (e) {
493
+ warn("Failed to spawn detached daemon: " + (e && e.message));
494
+ }
495
+ return false;
496
+ }
497
+
498
+ // ─── Wait for /health ───────────────────────────────────────────────────────
499
+
500
+ function waitForHealth(timeoutMs, cb) {
501
+ var start = Date.now();
502
+ function tick() {
503
+ tryHealth(PORT, function (ok) {
504
+ if (ok) return cb(true);
505
+ if (Date.now() - start > timeoutMs) return cb(false);
506
+ setTimeout(tick, 500);
507
+ });
508
+ }
509
+ tick();
510
+ }
511
+
512
+ // ─── Main ───────────────────────────────────────────────────────────────────
513
+
514
+ // ─── Wrapper-modelfile self-heal ───────────────────────────────────────────
515
+ // Old OA versions wrote `FROM <blob-path>` into open-agents-* wrappers,
516
+ // which strips TEMPLATE/RENDERER/PARSER metadata and breaks tools support.
517
+ // On upgrade, scan local Ollama for stale wrappers and rebuild them with
518
+ // `FROM <baseModel>`. Best-effort, silent on failure — postinstall must not
519
+ // fail npm install.
520
+ function repairBrokenWrappers() {
521
+ // Independent opt-out from daemon install — wrapper repair is a different
522
+ // concern and runs even when OA_SKIP_DAEMON_INSTALL=1, unless explicitly
523
+ // disabled.
524
+ if (process.env.OA_SKIP_WRAPPER_REPAIR === "1") return;
525
+ // Skip if ollama isn't on PATH.
526
+ var ollamaBin = "";
527
+ try {
528
+ ollamaBin = cp.execSync("command -v ollama 2>/dev/null || which ollama 2>/dev/null", {
529
+ encoding: "utf8", stdio: "pipe", timeout: 5000,
530
+ }).trim();
531
+ } catch (e) { /* ignore */ }
532
+ if (!ollamaBin) return;
533
+
534
+ // List models. Format is one per line: NAME ID SIZE MODIFIED.
535
+ var listOut = "";
536
+ try {
537
+ listOut = cp.execSync(ollamaBin + " list 2>/dev/null", {
538
+ encoding: "utf8", stdio: "pipe", timeout: 8000,
539
+ });
540
+ } catch (e) { return; }
541
+ var lines = listOut.split(/\r?\n/).filter(function (l) { return l.trim().length > 0; });
542
+ // Skip header row if present.
543
+ if (lines.length > 0 && /^NAME\s/i.test(lines[0])) lines = lines.slice(1);
544
+ var allNames = lines.map(function (l) { return l.split(/\s+/)[0]; });
545
+ var wrappers = allNames.filter(function (n) { return /^open-agents-/i.test(n); });
546
+ var bases = allNames.filter(function (n) { return !/^open-agents-/i.test(n); });
547
+ if (wrappers.length === 0) return;
548
+
549
+ function expandedName(base) {
550
+ // Mirror packages/cli/src/tui/setup.ts:expandedModelName.
551
+ var canonical = base.replace(/:latest$/i, "");
552
+ return "open-agents-" + canonical.replace(":", "-").replace(/\./g, "");
553
+ }
554
+ function legacyExpandedName(base) {
555
+ return "open-agents-" + base.replace(":", "-").replace(/\./g, "");
556
+ }
557
+ function stripTag(n) { return (n || "").replace(/:[^:]+$/, ""); }
558
+ function guessBase(wrapper) {
559
+ var stripped = stripTag(wrapper);
560
+ // Try the FULL base name first (e.g. "qwen3.6:27b") so size-tagged
561
+ // variants don't collide with each other. expandedName/legacyExpandedName
562
+ // handle the tag internally.
563
+ for (var i = 0; i < bases.length; i++) {
564
+ var name = bases[i];
565
+ if (expandedName(name) === stripped || legacyExpandedName(name) === stripped) {
566
+ return name;
567
+ }
568
+ }
569
+ // Then fall back to stripped-tag base — this is the only way to match
570
+ // wrappers that were built from `<base>:latest` and got their canonical
571
+ // form (`open-agents-<base-stripped>`). Prefer bases tagged ":latest"
572
+ // so that ambiguity (qwen3.6:27b vs qwen3.6:35b vs qwen3.6:latest)
573
+ // resolves to the user-facing default.
574
+ var latestFirst = bases.slice().sort(function (a, b) {
575
+ var aL = /:latest$/i.test(a) ? 0 : 1;
576
+ var bL = /:latest$/i.test(b) ? 0 : 1;
577
+ return aL - bL;
578
+ });
579
+ for (var j = 0; j < latestFirst.length; j++) {
580
+ var basis = stripTag(latestFirst[j]);
581
+ if (expandedName(basis) === stripped || legacyExpandedName(basis) === stripped) {
582
+ return latestFirst[j];
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+ function showCapabilities(name) {
588
+ try {
589
+ var out = cp.execSync(ollamaBin + " show " + name + " 2>/dev/null", {
590
+ encoding: "utf8", stdio: "pipe", timeout: 8000,
591
+ });
592
+ // Parse capabilities section
593
+ var inCaps = false;
594
+ var caps = [];
595
+ var rows = out.split(/\r?\n/);
596
+ for (var i = 0; i < rows.length; i++) {
597
+ var t = rows[i];
598
+ if (/^\s*Capabilities\s*$/.test(t)) { inCaps = true; continue; }
599
+ if (inCaps) {
600
+ if (/^\s*[A-Z][a-z]/.test(t) || t.trim() === "") { inCaps = false; continue; }
601
+ var v = t.trim();
602
+ if (v) caps.push(v.toLowerCase());
603
+ }
604
+ }
605
+ return caps;
606
+ } catch (e) { return null; }
607
+ }
608
+ function modelfileLooksBlob(name) {
609
+ try {
610
+ var mf = cp.execSync(ollamaBin + " show --modelfile " + name + " 2>/dev/null", {
611
+ encoding: "utf8", stdio: "pipe", timeout: 8000,
612
+ });
613
+ var m = mf.match(/^FROM\s+(.+)$/m);
614
+ if (!m) return false;
615
+ var f = m[1].trim();
616
+ return f.charAt(0) === "/" || /blobs\/sha256[-:]/.test(f);
617
+ } catch (e) { return false; }
618
+ }
619
+
620
+ var modelDir = path.join(HOME, ".open-agents", "models");
621
+ try { fs.mkdirSync(modelDir, { recursive: true }); } catch (e) {}
622
+
623
+ var rebuilt = 0;
624
+ var skipped = 0;
625
+ for (var i = 0; i < wrappers.length; i++) {
626
+ var w = wrappers[i];
627
+ var caps = showCapabilities(w);
628
+ var hasTools = caps !== null ? caps.indexOf("tools") !== -1 : true;
629
+ var blobby = modelfileLooksBlob(w);
630
+ if (hasTools && !blobby) { skipped++; continue; }
631
+ var base = guessBase(w);
632
+ if (!base) { skipped++; continue; }
633
+
634
+ // Read current num_ctx so we don't shrink it below what's already baked.
635
+ var numCtx = 32768;
636
+ try {
637
+ var pa = cp.execSync(ollamaBin + " show --parameters " + w + " 2>/dev/null", {
638
+ encoding: "utf8", stdio: "pipe", timeout: 5000,
639
+ });
640
+ var nm = pa.match(/num_ctx\s+(\d+)/);
641
+ if (nm) numCtx = Math.max(numCtx, parseInt(nm[1], 10));
642
+ } catch (e) {}
643
+ var numPredict = Math.min(16384, Math.max(2048, Math.floor(numCtx * 0.25)));
644
+ var content =
645
+ "FROM " + base + "\n" +
646
+ "PARAMETER num_ctx " + numCtx + "\n" +
647
+ "PARAMETER temperature 0\n" +
648
+ "PARAMETER num_predict " + numPredict + "\n" +
649
+ 'PARAMETER stop "<|endoftext|>"\n';
650
+ var mfPath = path.join(modelDir, "Modelfile." + stripTag(w));
651
+ try { fs.writeFileSync(mfPath, content, "utf8"); } catch (e) { skipped++; continue; }
652
+
653
+ try {
654
+ cp.execSync(ollamaBin + " create " + stripTag(w) + " -f " + JSON.stringify(mfPath) + " 2>&1", {
655
+ stdio: "pipe", timeout: 180000,
656
+ });
657
+ log("repaired wrapper: " + w + " (FROM " + base + ", num_ctx=" + numCtx + ")");
658
+ rebuilt++;
659
+ } catch (e) {
660
+ warn("failed to rebuild " + w + ": " + (e && e.message ? e.message.slice(0, 200) : "unknown"));
661
+ skipped++;
662
+ }
663
+ }
664
+
665
+ if (rebuilt > 0) {
666
+ log("Rebuilt " + rebuilt + " stale OA wrapper(s); " + skipped + " unchanged.");
667
+ }
668
+ }
669
+
670
+ function main() {
671
+ // Always do the nexus cleanup first, regardless of opt-out.
672
+ cleanNexus();
673
+
674
+ // Auto-repair stale OA model wrappers BEFORE restarting the daemon, so the
675
+ // freshly-restarted daemon picks up the rebuilt models on first inference.
676
+ try { repairBrokenWrappers(); } catch (e) {
677
+ warn("wrapper auto-repair crashed (non-fatal): " + (e && e.message));
678
+ }
679
+
680
+ if (process.env.OA_SKIP_DAEMON_INSTALL === "1") {
681
+ log("OA_SKIP_DAEMON_INSTALL=1 — skipping daemon service install.");
682
+ return safeExit(0);
683
+ }
684
+
685
+ // Force-kill any process holding the daemon port BEFORE we try to install
686
+ // a service. This handles the orphan-detached-daemon case (started by
687
+ // `oa serve --daemon` from a previous TUI session) — systemctl/launchctl
688
+ // restart can't reach it, so we have to clean up explicitly. Without this,
689
+ // the new service-managed daemon fails to bind port 11435 and the user
690
+ // ends up running stale in-memory code from the previous version.
691
+ forceKillPortHolder(PORT, function (killedCount) {
692
+ if (killedCount > 0) {
693
+ log("Killed " + killedCount + " stale daemon process(es) holding port " + PORT + ".");
694
+ }
695
+ runMainAfterKill();
696
+ });
697
+ }
698
+
699
+ function runMainAfterKill() {
700
+ // Fast path: daemon already answering /health on the target port.
701
+ // After forceKillPortHolder, this should be false unless a service manager
702
+ // already auto-restarted (rare race). We still check so the rest of the
703
+ // flow stays idempotent.
704
+ tryHealth(PORT, function (alreadyUp) {
705
+ var user = effectiveUser();
706
+ var nodeBin = resolveNodeBinary();
707
+ var oaScript = resolveOaBinary();
708
+
709
+ if (!oaScript) {
710
+ warn("Could not resolve `oa` launcher — daemon install skipped. After install completes, run `oa daemon install` manually.");
711
+ return safeExit(0);
712
+ }
713
+
714
+ if (alreadyUp) {
715
+ log("OA daemon answering /health on port " + PORT + " — re-checking version drift before service (re)install.");
716
+ // Note: we already force-killed any stale process — anything answering
717
+ // now is a fresh service-managed daemon. Service (re)install below is
718
+ // idempotent and ensures the unit file matches the current bundle.
719
+ }
720
+
721
+ log("Installing OA API daemon service for port " + PORT + " ...");
722
+ log(" node: " + nodeBin);
723
+ log(" oa script: " + oaScript);
724
+ log(" user: " + (user || "(unknown)"));
725
+
726
+ if (!user) {
727
+ warn("Running as bare root with no SUDO_USER — skipping per-user service install.");
728
+ warn("Re-run as your user, or run: OA_SKIP_DAEMON_INSTALL=0 npm i -g open-agents-ai");
729
+ // Still spawn detached so the port is live right now.
730
+ spawnDetached(nodeBin, oaScript);
731
+ return safeExit(0);
732
+ }
733
+
734
+ var ok = false;
735
+ if (IS_LINUX) {
736
+ ok = installSystemd(nodeBin, oaScript, user);
737
+ } else if (IS_MAC) {
738
+ ok = installLaunchd(nodeBin, oaScript, user);
739
+ } else if (IS_WIN) {
740
+ ok = installWindows(nodeBin, oaScript);
741
+ } else {
742
+ warn("Unsupported platform: " + PLATFORM + " — falling back to detached spawn.");
743
+ }
744
+
745
+ if (!ok) {
746
+ // Fallback: spawn a detached child so at least the port is live NOW.
747
+ spawnDetached(nodeBin, oaScript);
748
+ }
749
+
750
+ // Wait up to 15s for /health to come up, but don't fail npm install.
751
+ waitForHealth(15000, function (healthy) {
752
+ if (healthy) {
753
+ log("OA API daemon is live: http://127.0.0.1:" + PORT + "/health");
754
+ } else {
755
+ warn("OA API daemon did not answer /health within 15s. Check logs:");
756
+ if (IS_LINUX) warn(" systemctl --user status " + SERVICE_LABEL);
757
+ if (IS_MAC) warn(" launchctl print gui/$(id -u)/" + LAUNCHD_LABEL);
758
+ if (IS_WIN) warn(" schtasks /Query /TN " + WIN_TASK_NAME + " /V /FO LIST");
759
+ }
760
+ safeExit(0);
761
+ });
762
+ });
763
+ }
764
+
765
+ function safeExit(code) {
766
+ // Never exit non-zero from postinstall — it breaks `npm i -g`.
767
+ process.exit(typeof code === "number" ? 0 : 0);
768
+ }
769
+
770
+ // Guard against any unhandled throw — postinstall must not crash npm.
771
+ try {
772
+ main();
773
+ } catch (e) {
774
+ warn("postinstall crashed (non-fatal): " + (e && e.message));
775
+ safeExit(0);
776
+ }