@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.
@@ -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 launchctl against a fixed
8
- // service table — the request never reaches a shell, and an id that is not in
9
- // the table is refused, so nothing a browser sends can widen what this can do.
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 (!IS_MAC) return null;
131
- try {
132
- const { stdout } = await execFileP('launchctl', ['list']);
133
- return stdout.split('\n').some((l) => l.trim().endsWith(label));
134
- } catch { return null; }
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
- if (!IS_MAC) throw new Error('start/stop is wired for launchd (macOS) only');
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 batContent = `@echo off\r\ncd /d "${synapseDir}"\r\n${runCmd} >> "${stdoutLog}" 2>> "${stderrLog}"\r\n`;
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.4",
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": {