@hybridlabor-api/aos 4.6.4 → 4.6.5
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/bin/aos-dashboard.mjs +68 -9
- package/installer.js +70 -1
- package/package.json +1 -1
package/bin/aos-dashboard.mjs
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
//
|
|
5
5
|
// aos-dashboard [--port 7900] [--no-open]
|
|
6
6
|
//
|
|
7
|
-
// Binds to 127.0.0.1 only. Control actions run
|
|
8
|
-
//
|
|
9
|
-
// the
|
|
7
|
+
// Binds to 127.0.0.1 only. Control actions run against a fixed service table
|
|
8
|
+
// using launchctl on macOS or the corresponding Windows service wrapper/task —
|
|
9
|
+
// the request never reaches a shell, and an id that is not in the table is
|
|
10
|
+
// refused, so nothing a browser sends can widen what this can do.
|
|
10
11
|
|
|
11
12
|
import { createServer } from 'node:http';
|
|
12
13
|
import { connect, createServer as createTcpServer } from 'node:net';
|
|
@@ -22,6 +23,9 @@ const execFileP = promisify(execFile);
|
|
|
22
23
|
const HOME = os.homedir();
|
|
23
24
|
const h = (...p) => path.join(HOME, ...p);
|
|
24
25
|
const IS_MAC = process.platform === 'darwin';
|
|
26
|
+
const WINDOWS_STARTUP_DIR = process.env.APPDATA
|
|
27
|
+
? path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
28
|
+
: h('AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
25
29
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
30
|
const BRAND_DIR = path.join(PKG_ROOT, 'assets', 'brand');
|
|
27
31
|
|
|
@@ -104,6 +108,14 @@ const SERVICES = [
|
|
|
104
108
|
|
|
105
109
|
const byId = new Map(SERVICES.map((s) => [s.id, s]));
|
|
106
110
|
|
|
111
|
+
// Windows uses the VBS wrappers installed by the AOS installer for memB and
|
|
112
|
+
// Synapse. The process patterns are constants, never request data, so the
|
|
113
|
+
// dashboard cannot be turned into an arbitrary process launcher.
|
|
114
|
+
const WINDOWS_DAEMONS = {
|
|
115
|
+
memb: { vbs: 'com.bdb.memb.webui.vbs', processPattern: 'src[\\\\/]backend[\\\\/]server\\.py' },
|
|
116
|
+
synapse: { vbs: 'com.bdb.synapse.vbs', processPattern: 'synapse\\.js.*serve' },
|
|
117
|
+
};
|
|
118
|
+
|
|
107
119
|
// ------------------------------------------------------------------ probing
|
|
108
120
|
function portOpen(port, timeout = 800) {
|
|
109
121
|
return new Promise((resolve) => {
|
|
@@ -127,11 +139,23 @@ const freePort = () => new Promise((resolve, reject) => {
|
|
|
127
139
|
});
|
|
128
140
|
|
|
129
141
|
async function agentLoaded(label) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
if (IS_MAC) {
|
|
143
|
+
try {
|
|
144
|
+
const { stdout } = await execFileP('launchctl', ['list']);
|
|
145
|
+
return stdout.split('\n').some((l) => l.trim().endsWith(label));
|
|
146
|
+
} catch { return null; }
|
|
147
|
+
}
|
|
148
|
+
if (process.platform === 'win32' && label === 'com.bdb.openwiki.daemon') {
|
|
149
|
+
try {
|
|
150
|
+
const { stdout } = await execFileP('powershell.exe', [
|
|
151
|
+
'-NoProfile', '-Command',
|
|
152
|
+
"$task = Get-ScheduledTask -TaskName 'BDB_OpenWiki_Daemon' -ErrorAction SilentlyContinue; if ($task) { $task.State }",
|
|
153
|
+
], { timeout: 5000 });
|
|
154
|
+
const state = stdout.trim();
|
|
155
|
+
return state === 'Ready' || state === 'Running';
|
|
156
|
+
} catch { return null; }
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
const localVersion = (dir) => {
|
|
@@ -300,7 +324,42 @@ async function control(id, action) {
|
|
|
300
324
|
const svc = byId.get(id);
|
|
301
325
|
if (!svc) throw new Error('unknown service');
|
|
302
326
|
if (!['start', 'stop', 'restart'].includes(action)) throw new Error('unknown action');
|
|
303
|
-
|
|
327
|
+
|
|
328
|
+
if (process.platform === 'win32' && id === 'openwiki') {
|
|
329
|
+
// OpenWiki's refresh daemon is registered as a Scheduled Task on Windows,
|
|
330
|
+
// not as a launchd plist. Keep the command allowlisted and pass only a
|
|
331
|
+
// constant task name to PowerShell; browser input never becomes a command.
|
|
332
|
+
const run = (command) => execFileP('powershell.exe', [
|
|
333
|
+
'-NoProfile', '-Command', command,
|
|
334
|
+
], { timeout: 10000 }).catch((e) => { throw new Error(e.stderr?.trim() || e.message); });
|
|
335
|
+
const task = "'BDB_OpenWiki_Daemon'";
|
|
336
|
+
if (action === 'stop' || action === 'restart') {
|
|
337
|
+
await run(`Stop-ScheduledTask -TaskName ${task} -ErrorAction SilentlyContinue`);
|
|
338
|
+
}
|
|
339
|
+
if (action === 'start' || action === 'restart') {
|
|
340
|
+
await run(`Start-ScheduledTask -TaskName ${task} -ErrorAction Stop`);
|
|
341
|
+
}
|
|
342
|
+
return { ok: true };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (process.platform === 'win32' && WINDOWS_DAEMONS[id]) {
|
|
346
|
+
const daemon = WINDOWS_DAEMONS[id];
|
|
347
|
+
const vbsPath = path.join(WINDOWS_STARTUP_DIR, daemon.vbs);
|
|
348
|
+
if (!existsSync(vbsPath)) throw new Error(`no Windows startup wrapper installed for ${svc.name}`);
|
|
349
|
+
const run = (command) => execFileP('powershell.exe', [
|
|
350
|
+
'-NoProfile', '-Command', command,
|
|
351
|
+
], { timeout: 10000 }).catch((e) => { throw new Error(e.stderr?.trim() || e.message); });
|
|
352
|
+
if (action === 'stop' || action === 'restart') {
|
|
353
|
+
const escapedPattern = daemon.processPattern.replace(/'/g, "''");
|
|
354
|
+
await run(`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine -match '${escapedPattern}' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`);
|
|
355
|
+
}
|
|
356
|
+
if (action === 'start' || action === 'restart') {
|
|
357
|
+
await execFileP('wscript.exe', [vbsPath], { timeout: 10000 });
|
|
358
|
+
}
|
|
359
|
+
return { ok: true };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!IS_MAC) throw new Error('start/stop is not available on this platform for this service');
|
|
304
363
|
|
|
305
364
|
const plist = h('Library', 'LaunchAgents', `${svc.agent}.plist`);
|
|
306
365
|
if (!existsSync(plist)) throw new Error(`no LaunchAgent installed for ${svc.name}`);
|
package/installer.js
CHANGED
|
@@ -241,6 +241,42 @@ function hasExecutable(binary) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// Windows installers launched from an existing terminal can inherit a stale
|
|
245
|
+
// PATH after Go was installed through winget or the official installer. The
|
|
246
|
+
// Synapse JS launcher shells out to Go on first run, so discover the standard
|
|
247
|
+
// install locations as well as PATH and persist the resolved bin directory in
|
|
248
|
+
// the generated startup wrapper.
|
|
249
|
+
function findWindowsGoBin() {
|
|
250
|
+
if (process.platform !== 'win32') return null;
|
|
251
|
+
const candidates = [];
|
|
252
|
+
try {
|
|
253
|
+
const found = execSync('where.exe go', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
254
|
+
.split(/\r?\n/).map(s => s.trim()).find(Boolean);
|
|
255
|
+
if (found) candidates.push(path.dirname(found));
|
|
256
|
+
} catch (_) { /* PATH can be stale immediately after installation */ }
|
|
257
|
+
|
|
258
|
+
const roots = [
|
|
259
|
+
process.env.GOROOT,
|
|
260
|
+
process.env.ProgramW6432,
|
|
261
|
+
process.env.ProgramFiles,
|
|
262
|
+
process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs') : null,
|
|
263
|
+
].filter(Boolean);
|
|
264
|
+
for (const root of roots) {
|
|
265
|
+
candidates.push(path.join(root, 'Go', 'bin'));
|
|
266
|
+
candidates.push(path.join(root, 'go', 'bin'));
|
|
267
|
+
}
|
|
268
|
+
return candidates.find(dir => fs.existsSync(path.join(dir, 'go.exe'))) || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function verifyPythonImports(pythonPath, cwd, importCode) {
|
|
272
|
+
const result = spawnSync(pythonPath, ['-c', importCode], {
|
|
273
|
+
cwd,
|
|
274
|
+
stdio: 'ignore',
|
|
275
|
+
timeout: 30000,
|
|
276
|
+
});
|
|
277
|
+
return !result.error && result.status === 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
244
280
|
function resolveUnsupportedMcpConfigKeys() {
|
|
245
281
|
const keys = unsupportedMcpConfigKeys.slice();
|
|
246
282
|
conditionalMcpConfigKeys.forEach(entry => {
|
|
@@ -1864,6 +1900,30 @@ async function installMemB(interactive) {
|
|
|
1864
1900
|
if (installWebUI && fs.existsSync(serverPy)) {
|
|
1865
1901
|
runPipWithRetry(installCmd('fastapi uvicorn'), { cwd: membDir, stdio: 'ignore' }, 'pip fastapi+uvicorn for memB WebUI', 2, 120000);
|
|
1866
1902
|
}
|
|
1903
|
+
|
|
1904
|
+
// A partially upgraded venv can contain pydantic while missing or
|
|
1905
|
+
// mismatching pydantic-core. That failure only appears later when
|
|
1906
|
+
// the WebUI imports its models, so validate the actual interpreter
|
|
1907
|
+
// before registering the daemon and repair the pair in place.
|
|
1908
|
+
const importCheck = installWebUI && fs.existsSync(serverPy)
|
|
1909
|
+
? 'import pydantic, pydantic_core; from fastapi import FastAPI; import uvicorn'
|
|
1910
|
+
: 'import pydantic, pydantic_core';
|
|
1911
|
+
if (!verifyPythonImports(venvPython, membDir, importCheck)) {
|
|
1912
|
+
log.warn('memB Python dependencies are inconsistent; repairing pydantic and pydantic-core.');
|
|
1913
|
+
const repairPackages = installWebUI && fs.existsSync(serverPy)
|
|
1914
|
+
? '--force-reinstall "pydantic>=2.7.3" "pydantic-core>=2.18.4" fastapi uvicorn'
|
|
1915
|
+
: '--force-reinstall "pydantic>=2.7.3" "pydantic-core>=2.18.4"';
|
|
1916
|
+
const repaired = runPipWithRetry(
|
|
1917
|
+
installCmd(repairPackages),
|
|
1918
|
+
{ cwd: membDir, stdio: 'inherit' },
|
|
1919
|
+
'repair memB Python dependencies',
|
|
1920
|
+
2,
|
|
1921
|
+
900000,
|
|
1922
|
+
);
|
|
1923
|
+
if (repaired && !verifyPythonImports(venvPython, membDir, importCheck)) {
|
|
1924
|
+
log.warn('memB dependency repair did not pass the import check; inspect the venv before starting the daemon.');
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1867
1927
|
} catch (e) {
|
|
1868
1928
|
log.warn(`Failed memB standalone venv setup: ${e.message}`);
|
|
1869
1929
|
}
|
|
@@ -2017,11 +2077,20 @@ async function installSynapse() {
|
|
|
2017
2077
|
const stderrLog = path.join(synapseLogDir, 'daemon.stderr.log');
|
|
2018
2078
|
const batPath = path.join(synapseLogDir, 'run-synapse.bat');
|
|
2019
2079
|
const runCmd = binaryPath.endsWith('.js') ? `node "${binaryPath}" serve --port 7781` : `"${binaryPath}" serve --port 7781`;
|
|
2080
|
+
const goBin = binaryPath.endsWith('.js') ? findWindowsGoBin() : null;
|
|
2081
|
+
if (binaryPath.endsWith('.js') && goBin) {
|
|
2082
|
+
log.step(`Synapse Windows wrapper will use Go from ${goBin}`);
|
|
2083
|
+
} else if (binaryPath.endsWith('.js')) {
|
|
2084
|
+
log.warn('Synapse Windows launcher requires Go on first run; install Go from https://go.dev/dl/ before starting the daemon.');
|
|
2085
|
+
}
|
|
2020
2086
|
// WshShell.Run has no stdout/stderr redirection of its own, so a crash on launch
|
|
2021
2087
|
// (e.g. a missing dependency the binary shells out to) used to die silently with
|
|
2022
2088
|
// nothing to diagnose short of reading source — route through a .bat wrapper that
|
|
2023
2089
|
// redirects to the same ~/.synapse log files the macOS launchd plist already writes.
|
|
2024
|
-
const
|
|
2090
|
+
const goSetup = goBin
|
|
2091
|
+
? `set "PATH=${goBin.replace(/%/g, '%%')};%PATH%"\r\n`
|
|
2092
|
+
: `where.exe go >nul 2>&1 || (echo Go is required by the Synapse launcher. Install it from https://go.dev/dl/ and restart this daemon. >> "${stderrLog}" & exit /b 1)\r\n`;
|
|
2093
|
+
const batContent = `@echo off\r\n${goSetup}cd /d "${synapseDir}"\r\n${runCmd} >> "${stdoutLog}" 2>> "${stderrLog}"\r\n`;
|
|
2025
2094
|
const vbsPath = path.join(startupDir, 'com.bdb.synapse.vbs');
|
|
2026
2095
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.CurrentDirectory = "${synapseDir}"\r\nWshShell.Run """${batPath}""", 0, False\r\n`;
|
|
2027
2096
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybridlabor-api/aos",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.5",
|
|
4
4
|
"description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
|
|
5
5
|
"main": "installer.js",
|
|
6
6
|
"engines": {
|