@hybridlabor-api/aos 4.6.4 → 4.6.6

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.
@@ -64,8 +64,10 @@ export function propose() {
64
64
  };
65
65
  }
66
66
 
67
+ import { pathToFileURL } from 'node:url';
68
+
67
69
  // ------------------------------------------------------------------- CLI
68
- if (import.meta.url === `file://${process.argv[1]}`) {
70
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
69
71
  const [cmd, key, ...rest] = process.argv.slice(2);
70
72
  const value = rest.join(' ');
71
73
 
@@ -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 {
@@ -2477,6 +2546,11 @@ function verifyEcosystemInstallation() {
2477
2546
  console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.dim}⚪ Optional / Not downloaded${colors.reset}`);
2478
2547
  }
2479
2548
  }
2549
+
2550
+ console.log('');
2551
+ console.log(` ${colors.bold}━━━ BDB Agent OS Dashboard ━━━${colors.reset}`);
2552
+ console.log(` Interactive Control Center: npx aos-dashboard`);
2553
+ console.log(` ${colors.dim}(Live status & service control at http://127.0.0.1:7900)${colors.reset}`);
2480
2554
  }
2481
2555
 
2482
2556
  function resolveTargetPaths(platformValue, customPaths) {
@@ -3490,6 +3564,51 @@ function installGlobalHooks() {
3490
3564
  log.warn(`Could not install OpenCode plugin: ${e.message}`);
3491
3565
  }
3492
3566
  }
3567
+
3568
+ // 5. Global CLI launcher binaries (aos-config, aos-dashboard, aos-uninstall)
3569
+ installGlobalBinaries();
3570
+ }
3571
+
3572
+ function installGlobalBinaries() {
3573
+ const binSrc = path.join(srcDir, 'bin');
3574
+ const globalAgentsBin = path.join(homeDir, '.agents', 'bin');
3575
+ if (fs.existsSync(binSrc)) {
3576
+ copyDirRecursiveSync(binSrc, globalAgentsBin);
3577
+ log.step(`Installed CLI binaries to ${globalAgentsBin}`);
3578
+ }
3579
+
3580
+ const localBinDir = path.join(homeDir, '.local', 'bin');
3581
+ if (!fs.existsSync(localBinDir)) {
3582
+ try { fs.mkdirSync(localBinDir, { recursive: true }); } catch (e) { logDebug(e, 'mkdir localBin'); }
3583
+ }
3584
+
3585
+ const isWin = process.platform === 'win32';
3586
+ const cliBins = ['aos-config', 'aos-dashboard', 'aos-uninstall'];
3587
+
3588
+ for (const name of cliBins) {
3589
+ const targetMjs = path.join(globalAgentsBin, `${name}.mjs`);
3590
+ if (!fs.existsSync(targetMjs)) continue;
3591
+
3592
+ // Shell wrapper for Unix / Git Bash
3593
+ const shPath = path.join(localBinDir, name);
3594
+ const shContent = `#!/bin/sh\nexec node "${targetMjs}" "$@"\n`;
3595
+ try {
3596
+ fs.writeFileSync(shPath, shContent, { mode: 0o755 });
3597
+ try { fs.chmodSync(shPath, 0o755); } catch (e) { logDebug(e, `chmod ${shPath}`); }
3598
+ } catch (e) { logDebug(e, `write ${shPath}`); }
3599
+
3600
+ // Windows CMD and PowerShell launchers
3601
+ if (isWin) {
3602
+ const cmdPath = path.join(localBinDir, `${name}.cmd`);
3603
+ const cmdContent = `@echo off\r\nnode "${targetMjs}" %*\r\n`;
3604
+ try { fs.writeFileSync(cmdPath, cmdContent); } catch (e) { logDebug(e, `write ${cmdPath}`); }
3605
+
3606
+ const ps1Path = path.join(localBinDir, `${name}.ps1`);
3607
+ const ps1Content = `node "${targetMjs}" $args\r\n`;
3608
+ try { fs.writeFileSync(ps1Path, ps1Content); } catch (e) { logDebug(e, `write ${ps1Path}`); }
3609
+ }
3610
+ }
3611
+ log.step(`Wired CLI launcher binaries (aos-config, aos-dashboard, aos-uninstall) in ${localBinDir}`);
3493
3612
  }
3494
3613
 
3495
3614
  // Merge the BDB hooks -- the two gates plus the memB ambient-memory hook --
@@ -3885,26 +4004,59 @@ async function promptOptionalModules(installedModules) {
3885
4004
  return chosen;
3886
4005
  }
3887
4006
 
3888
- function generateAndOpenLaunchpad() {
4007
+ // Module ids whose daemons ship a web interface or background service worth
4008
+ // surfacing in the launchpad (each has a card in the generated HTML below).
4009
+ const LAUNCHPAD_WEB_MODULES = ['memb', 'synapse', 'openwiki', 'ao', 'remote'];
4010
+
4011
+ // Explicit opt-in flags that open the launchpad even outside a dev checkout.
4012
+ const LAUNCHPAD_OPEN_FLAGS = ['--launchpad', '--dashboard', '--open', '--dev'];
4013
+
4014
+ // True on a local BDB dev checkout (or BDB_DEV=1). This is intentionally
4015
+ // narrower than the old isDevWorkflow check: CLI flags live in
4016
+ // shouldOpenLaunchpad(), not here, so callers can tell "dev machine" apart
4017
+ // from "user asked for it". Overrides exist for tests.
4018
+ function isDevEnvironment(overrides = {}) {
4019
+ const home = overrides.homeDir || homeDir;
4020
+ const env = overrides.env || process.env;
4021
+ return fs.existsSync(path.join(home, 'dev', 'bdb-dev')) ||
4022
+ fs.existsSync(path.join(home, 'bdb-dev')) ||
4023
+ env.BDB_DEV === '1';
4024
+ }
4025
+
4026
+ // True when the launchpad should be generated and opened: a dev checkout, an
4027
+ // explicit flag, or installed daemon/WebUI modules. Overrides exist for tests.
4028
+ function shouldOpenLaunchpad(installedModules = [], overrides = {}) {
4029
+ const argv = overrides.argv || process.argv;
4030
+ if (isDevEnvironment(overrides)) return true;
4031
+ if (LAUNCHPAD_OPEN_FLAGS.some(f => argv.includes(f))) return true;
4032
+ const installed = Array.isArray(installedModules) ? installedModules : [];
4033
+ return installed.some(m => LAUNCHPAD_WEB_MODULES.includes(m));
4034
+ }
4035
+
4036
+ // Boot autostart (login item) is opt-in only: an explicit flag or a dev
4037
+ // checkout. Regular end-user installs must not get an autostart entry that
4038
+ // opens a browser on every login. Overrides exist for tests.
4039
+ function shouldAutostartLaunchpad(overrides = {}) {
4040
+ const argv = overrides.argv || process.argv;
4041
+ if (argv.includes('--autostart-launchpad')) return true;
4042
+ return isDevEnvironment(overrides);
4043
+ }
4044
+
4045
+ function generateAndOpenLaunchpad(installedModules = []) {
3889
4046
  if (DRY_RUN) {
3890
4047
  log.step('[dry-run] would generate & open BDB Launchpad HTML');
3891
- return;
4048
+ return false;
3892
4049
  }
3893
- if (process.env.SSH_CLIENT || process.env.SSH_TTY) return;
4050
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY) return false;
3894
4051
 
3895
- // Local Developer Workflow Guard:
3896
- // Only generate and open the launchpad if running in a local developer repo environment or explicitly requested.
4052
+ // Decoupled from the dev-checkout layout: on a regular end-user machine
4053
+ // (no ~/dev/bdb-dev, no ~/bdb-dev) the launchpad still opens when daemon
4054
+ // modules are installed or an explicit flag was passed.
3897
4055
  // ~/bdb-dev was the pre-reorg workspace root; the State-0 reorg moved it to
3898
- // ~/dev/bdb-dev, but this check was never updated -- it has been silently
3899
- // false on every real dev machine since that reorg. Checking both keeps
3900
- // this working regardless of which layout a given machine still has.
3901
- const isDevWorkflow = fs.existsSync(path.join(homeDir, 'dev', 'bdb-dev')) ||
3902
- fs.existsSync(path.join(homeDir, 'bdb-dev')) ||
3903
- process.env.BDB_DEV === '1' ||
3904
- process.argv.includes('--launchpad') ||
3905
- process.argv.includes('--dev');
3906
- if (!isDevWorkflow) {
3907
- return;
4056
+ // ~/dev/bdb-dev. Both are checked so this works regardless of which layout
4057
+ // a given machine still has.
4058
+ if (!shouldOpenLaunchpad(installedModules)) {
4059
+ return false;
3908
4060
  }
3909
4061
 
3910
4062
  const html = `<!DOCTYPE html>
@@ -4240,6 +4392,12 @@ function generateAndOpenLaunchpad() {
4240
4392
  // own background services, but the dashboard itself (a static file, not
4241
4393
  // a process) never got the equivalent: something to open it automatically
4242
4394
  // on login too, not just right after an installer run.
4395
+ // Opt-in only: without --autostart-launchpad (or a dev checkout) a regular
4396
+ // end-user install must not drop a login item that opens a browser on
4397
+ // every boot.
4398
+ if (!shouldAutostartLaunchpad()) {
4399
+ return true;
4400
+ }
4243
4401
  if (process.platform === 'darwin') {
4244
4402
  const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', 'com.bdb.launchpad.plist');
4245
4403
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
@@ -4275,9 +4433,10 @@ function generateAndOpenLaunchpad() {
4275
4433
  fs.writeFileSync(vbsPath, vbsContent, 'utf-8');
4276
4434
  } catch (e) { logDebug(e, 'launchpad autostart vbs'); }
4277
4435
  }
4436
+ return true;
4278
4437
  }
4279
4438
 
4280
- async function universalHarnessSync(primaryMcpConfigPath) {
4439
+ async function universalHarnessSync(primaryMcpConfigPath, installedModules = []) {
4281
4440
  log.info('Universal Agent Harness Sync...');
4282
4441
  const detections = detectPlatforms();
4283
4442
  let masterMcpData = {};
@@ -4439,7 +4598,7 @@ async function universalHarnessSync(primaryMcpConfigPath) {
4439
4598
  }
4440
4599
  }
4441
4600
  log.success('Universal Sync Complete!');
4442
- generateAndOpenLaunchpad();
4601
+ generateAndOpenLaunchpad(installedModules);
4443
4602
  }
4444
4603
 
4445
4604
  async function runQuickUpdate(installState) {
@@ -4545,7 +4704,7 @@ async function runQuickUpdate(installState) {
4545
4704
  // did, so a dev-workflow machine only ever saw it once, on day one --
4546
4705
  // every subsequent run is a Quick Update, which is what almost every
4547
4706
  // real run after the first actually is.
4548
- generateAndOpenLaunchpad();
4707
+ generateAndOpenLaunchpad(modulesToUpdate);
4549
4708
  }
4550
4709
 
4551
4710
 
@@ -4944,7 +5103,7 @@ async function main() {
4944
5103
  saveManifest({ tier, isUniversal: wantsUniversal, installedModules: installedModulesForPrompt });
4945
5104
 
4946
5105
  if (wantsUniversal) {
4947
- await universalHarnessSync(primaryTarget.mcpConfigPath);
5106
+ await universalHarnessSync(primaryTarget.mcpConfigPath, installedModulesForPrompt);
4948
5107
  }
4949
5108
 
4950
5109
  // Flush file-level install manifest after all writes are done.
@@ -4992,4 +5151,11 @@ module.exports = {
4992
5151
  loadPipelineConfig,
4993
5152
  resolveAgentConfig,
4994
5153
  CANONICAL_TIERS,
5154
+ // Launchpad lifecycle (exported for regression tests)
5155
+ generateAndOpenLaunchpad,
5156
+ shouldOpenLaunchpad,
5157
+ shouldAutostartLaunchpad,
5158
+ isDevEnvironment,
5159
+ LAUNCHPAD_WEB_MODULES,
5160
+ LAUNCHPAD_OPEN_FLAGS,
4995
5161
  };
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.6",
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": {
@@ -55,6 +55,17 @@ function checkGit() {
55
55
 
56
56
  // ---------------------------------------------------------------- aos config
57
57
  function checkAosConfig() {
58
+ const machineConfig = (() => {
59
+ try { return JSON.parse(readFileSync(path.join(HOME, '.agents', 'aos-config.json'), 'utf8')); }
60
+ catch { return null; }
61
+ })();
62
+ const machineOk = !!(machineConfig?.workspaceRoot && machineConfig?.domains?.length && machineConfig?.userId);
63
+ add('aos', 'machine vocabulary', machineOk,
64
+ machineOk
65
+ ? `workspaceRoot "${machineConfig.workspaceRoot}" · ${machineConfig.domains.length} domains · userId "${machineConfig.userId}"`
66
+ : 'missing or incomplete in ~/.agents/aos-config.json — /aos-project-init requires machine vocabulary',
67
+ 'Run /aos-setup (or `aos-config propose` / `aos-config set`) to configure machine vocabulary.');
68
+
58
69
  add('aos', '.aos/project.json', !!aosConfig,
59
70
  aosConfig
60
71
  ? `slug "${aosConfig.slug}"${aosConfig.domain ? ` · domain ${aosConfig.domain}` : ''}${aosConfig.watch ? ' · überwacht' : ''}`