@hybridlabor-api/aos 4.6.3 → 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 +125 -8
- 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 => {
|
|
@@ -1072,6 +1108,15 @@ function moveIfExists(src, dest, label) {
|
|
|
1072
1108
|
}
|
|
1073
1109
|
}
|
|
1074
1110
|
|
|
1111
|
+
function safeRmDirSync(dirPath, maxRetries = 5, retryDelay = 200) {
|
|
1112
|
+
if (!dirPath || !fs.existsSync(dirPath)) return;
|
|
1113
|
+
try {
|
|
1114
|
+
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries, retryDelay });
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
logDebug(err, `safeRmDirSync: ${dirPath}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1075
1120
|
function copyDirRecursiveSync(source, target, excludeList = [], manifest = null, knownSourceHashes = null) {
|
|
1076
1121
|
// Fall back to session-level state when no explicit manifest is passed.
|
|
1077
1122
|
const mfst = manifest !== null ? manifest : _sessionManifest;
|
|
@@ -1722,7 +1767,21 @@ function downloadOrUpdateModule(pkgName, targetDir, displayName) {
|
|
|
1722
1767
|
// kept until the swap succeeds, and restored if it does not.
|
|
1723
1768
|
const stagingDir = `${targetDir}.incoming-${timestamp}`;
|
|
1724
1769
|
const retiredDir = `${targetDir}.previous-${timestamp}`;
|
|
1725
|
-
|
|
1770
|
+
|
|
1771
|
+
// Clean up any stale staging or retired directories left behind by earlier interrupted runs
|
|
1772
|
+
try {
|
|
1773
|
+
const parentDir = path.dirname(targetDir);
|
|
1774
|
+
const baseName = path.basename(targetDir);
|
|
1775
|
+
if (fs.existsSync(parentDir)) {
|
|
1776
|
+
for (const item of fs.readdirSync(parentDir)) {
|
|
1777
|
+
if (item.startsWith(`${baseName}.incoming-`) || item.startsWith(`${baseName}.previous-`)) {
|
|
1778
|
+
safeRmDirSync(path.join(parentDir, item));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
} catch (_) {}
|
|
1783
|
+
|
|
1784
|
+
safeRmDirSync(stagingDir);
|
|
1726
1785
|
fs.mkdirSync(stagingDir, { recursive: true });
|
|
1727
1786
|
execSync(`tar -xzf "${path.join(targetDir, tarball)}" --strip-components=1 -C "${stagingDir}"`, { stdio: 'ignore' });
|
|
1728
1787
|
fs.unlinkSync(path.join(targetDir, tarball));
|
|
@@ -1733,7 +1792,11 @@ function downloadOrUpdateModule(pkgName, targetDir, displayName) {
|
|
|
1733
1792
|
for (const keep of PRESERVE) {
|
|
1734
1793
|
const from = path.join(targetDir, keep);
|
|
1735
1794
|
if (fs.existsSync(from) && !fs.existsSync(path.join(stagingDir, keep))) {
|
|
1736
|
-
|
|
1795
|
+
try {
|
|
1796
|
+
fs.renameSync(from, path.join(stagingDir, keep));
|
|
1797
|
+
} catch (preserveErr) {
|
|
1798
|
+
logDebug(preserveErr, `preserve ${keep}`);
|
|
1799
|
+
}
|
|
1737
1800
|
}
|
|
1738
1801
|
}
|
|
1739
1802
|
|
|
@@ -1741,11 +1804,17 @@ function downloadOrUpdateModule(pkgName, targetDir, displayName) {
|
|
|
1741
1804
|
fs.renameSync(targetDir, retiredDir);
|
|
1742
1805
|
fs.renameSync(stagingDir, targetDir);
|
|
1743
1806
|
} catch (swapError) {
|
|
1744
|
-
if (fs.existsSync(retiredDir) && !fs.existsSync(targetDir))
|
|
1745
|
-
|
|
1807
|
+
if (fs.existsSync(retiredDir) && !fs.existsSync(targetDir)) {
|
|
1808
|
+
try { fs.renameSync(retiredDir, targetDir); } catch (_) {}
|
|
1809
|
+
}
|
|
1810
|
+
safeRmDirSync(stagingDir);
|
|
1746
1811
|
throw swapError;
|
|
1747
1812
|
}
|
|
1748
|
-
|
|
1813
|
+
|
|
1814
|
+
// On Windows especially, deleting a directory that was just renamed or contains
|
|
1815
|
+
// locked file handles can throw EPERM / EBUSY. Since targetDir already has the
|
|
1816
|
+
// new version in place, failure to clean up retiredDir must be non-fatal.
|
|
1817
|
+
safeRmDirSync(retiredDir);
|
|
1749
1818
|
|
|
1750
1819
|
s.stop(`${displayName} ready (v${status.remoteVer || 'latest'})`);
|
|
1751
1820
|
return true;
|
|
@@ -1810,6 +1879,17 @@ async function installMemB(interactive) {
|
|
|
1810
1879
|
if (!hasUv) {
|
|
1811
1880
|
try { execSync('uv --version', { stdio: 'ignore' }); hasUv = true; } catch (e) { hasUv = false; }
|
|
1812
1881
|
}
|
|
1882
|
+
if (!hasUv && fs.existsSync(venvPython)) {
|
|
1883
|
+
try {
|
|
1884
|
+
execSync(`"${venvPython}" -m pip --version`, { stdio: 'ignore' });
|
|
1885
|
+
} catch (_) {
|
|
1886
|
+
try {
|
|
1887
|
+
execSync(`"${venvPython}" -m ensurepip --default-pip`, { cwd: membDir, stdio: 'ignore' });
|
|
1888
|
+
} catch (ePip) {
|
|
1889
|
+
logDebug(ePip, 'ensurepip bootstrap for memB');
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1813
1893
|
const uvInstall = (pkgsArg) => `uv pip install --python "${venvPython}" ${pkgsArg}`;
|
|
1814
1894
|
const pipInstall = (pkgsArg) => `"${venvPython}" -m pip install ${pkgsArg} --timeout 30 --no-input`;
|
|
1815
1895
|
const installCmd = (pkgsArg) => hasUv ? uvInstall(pkgsArg) : pipInstall(pkgsArg);
|
|
@@ -1820,6 +1900,30 @@ async function installMemB(interactive) {
|
|
|
1820
1900
|
if (installWebUI && fs.existsSync(serverPy)) {
|
|
1821
1901
|
runPipWithRetry(installCmd('fastapi uvicorn'), { cwd: membDir, stdio: 'ignore' }, 'pip fastapi+uvicorn for memB WebUI', 2, 120000);
|
|
1822
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
|
+
}
|
|
1823
1927
|
} catch (e) {
|
|
1824
1928
|
log.warn(`Failed memB standalone venv setup: ${e.message}`);
|
|
1825
1929
|
}
|
|
@@ -1973,11 +2077,20 @@ async function installSynapse() {
|
|
|
1973
2077
|
const stderrLog = path.join(synapseLogDir, 'daemon.stderr.log');
|
|
1974
2078
|
const batPath = path.join(synapseLogDir, 'run-synapse.bat');
|
|
1975
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
|
+
}
|
|
1976
2086
|
// WshShell.Run has no stdout/stderr redirection of its own, so a crash on launch
|
|
1977
2087
|
// (e.g. a missing dependency the binary shells out to) used to die silently with
|
|
1978
2088
|
// nothing to diagnose short of reading source — route through a .bat wrapper that
|
|
1979
2089
|
// redirects to the same ~/.synapse log files the macOS launchd plist already writes.
|
|
1980
|
-
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`;
|
|
1981
2094
|
const vbsPath = path.join(startupDir, 'com.bdb.synapse.vbs');
|
|
1982
2095
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.CurrentDirectory = "${synapseDir}"\r\nWshShell.Run """${batPath}""", 0, False\r\n`;
|
|
1983
2096
|
try {
|
|
@@ -2076,11 +2189,12 @@ async function installOpenWikiVisualizer() {
|
|
|
2076
2189
|
const ecosystemWiki = path.join(homeDir, '.openwiki', 'ecosystem-wiki');
|
|
2077
2190
|
const syncScript = path.join(srcDir, 'skills', 'global_config', 'openwiki-skill', 'scripts', 'sync_ecosystem_wiki.py');
|
|
2078
2191
|
if (!wikiPath) {
|
|
2192
|
+
const pyBin = hasExecutable('python3') ? 'python3' : hasExecutable('python') ? 'python' : null;
|
|
2079
2193
|
if (looksLikeWiki(ecosystemWiki)) {
|
|
2080
2194
|
wikiPath = ecosystemWiki;
|
|
2081
|
-
} else if (fs.existsSync(syncScript) &&
|
|
2195
|
+
} else if (fs.existsSync(syncScript) && pyBin) {
|
|
2082
2196
|
try {
|
|
2083
|
-
execSync(`
|
|
2197
|
+
execSync(`"${pyBin}" "${syncScript}"`, { stdio: 'ignore' });
|
|
2084
2198
|
if (looksLikeWiki(ecosystemWiki)) wikiPath = ecosystemWiki;
|
|
2085
2199
|
} catch (e) { logDebug(e, 'ecosystem wiki sync'); }
|
|
2086
2200
|
}
|
|
@@ -2091,6 +2205,9 @@ async function installOpenWikiVisualizer() {
|
|
|
2091
2205
|
});
|
|
2092
2206
|
if (!isCancel(answer) && answer && answer.trim()) {
|
|
2093
2207
|
const candidate = answer.trim().replace(/^~(?=$|\/|\\)/, homeDir);
|
|
2208
|
+
if (!fs.existsSync(candidate)) {
|
|
2209
|
+
try { fs.mkdirSync(candidate, { recursive: true }); } catch (_) {}
|
|
2210
|
+
}
|
|
2094
2211
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
2095
2212
|
wikiPath = candidate;
|
|
2096
2213
|
if (!looksLikeWiki(candidate)) {
|
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": {
|