@hybridlabor-api/aos 4.13.0 → 4.13.2

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.
@@ -129,9 +129,11 @@ function main() {
129
129
  try {
130
130
  input = JSON.parse(rawInput);
131
131
  } catch (e) {
132
- // If we cannot parse hook input, fail closed
133
- respond(true, false, `could not parse hook input: ${e.message}`, "unknown");
134
- return;
132
+ // Harness unknown: deny JSON on stdout (Antigravity) plus stderr + exit 2 (Claude Code/Codex) closes every path.
133
+ const reason = `could not parse hook input: ${e.message}`;
134
+ console.log(JSON.stringify({ decision: "deny", reason: `[MECHANICAL GO-GATE BLOCKED] ${reason}` }));
135
+ process.stderr.write(`Blocked by go-gate hook: ${reason}\n`);
136
+ process.exit(2);
135
137
  }
136
138
  }
137
139
 
package/README.de.md CHANGED
@@ -411,10 +411,6 @@ flowchart LR
411
411
 
412
412
  `@hybridlabor-api/heimdall-token-saver`: komprimiert wiederholte CLI-Ausgabe über Ambient Hooks auf jedem Harness. Reduziert Token-Overhead bei großen Projekten. Skill: `token-saver-config`.
413
413
 
414
- ![Token savings with Heimdall Token Saver](assets/bdb_savings_graph_sketch.jpg)
415
-
416
- *Illustrative Skizze aus v3.x. Die Prozentwerte sind Schätzungen des Projekts, keine für dieses README durchgeführten Messungen.*
417
-
418
414
  ---
419
415
 
420
416
  ## Aktualisierung
package/README.md CHANGED
@@ -411,10 +411,6 @@ flowchart LR
411
411
 
412
412
  `@hybridlabor-api/heimdall-token-saver`: compresses repeated CLI output via ambient hooks on every harness. Reduces token overhead on large projects. Skill: `token-saver-config`.
413
413
 
414
- ![Token savings with Heimdall Token Saver](assets/bdb_savings_graph_sketch.jpg)
415
-
416
- *Illustrative sketch from v3.x. The percentages are the project's own estimates, not measurements made for this README.*
417
-
418
414
  ---
419
415
 
420
416
  ## Updating
package/README.pt.md CHANGED
@@ -411,10 +411,6 @@ flowchart LR
411
411
 
412
412
  `@hybridlabor-api/heimdall-token-saver`: comprime saída CLI repetida via hooks ambiente em todo harness. Reduz sobrecarga de token em projetos grandes. Skill: `token-saver-config`.
413
413
 
414
- ![Token savings with Heimdall Token Saver](assets/bdb_savings_graph_sketch.jpg)
415
-
416
- *Esboço ilustrativo da v3.x. Os percentuais são estimativas do próprio projeto, não medições feitas para este README.*
417
-
418
414
  ---
419
415
 
420
416
  ## Atualizando
@@ -244,7 +244,7 @@ function execute(p) {
244
244
  if (existsSync(settings)) {
245
245
  try {
246
246
  const s = JSON.parse(readFileSync(settings, 'utf8'));
247
- const bdb = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs'];
247
+ const bdb = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
248
248
  let touched = false;
249
249
  for (const [event, entries] of Object.entries(s.hooks || {})) {
250
250
  const kept = entries.filter((e) => !(e.hooks || []).some((x) => bdb.some((n) => String(x.command).includes(n))));
package/installer.js CHANGED
@@ -526,12 +526,24 @@ function loadInstallManifest(customPath = null) {
526
526
  return {};
527
527
  }
528
528
 
529
+ // Temp file in the same directory + rename: a crash mid-write cannot leave a truncated manifest.
530
+ function writeFileAtomic(filePath, data) {
531
+ const tmp = `${filePath}.${process.pid}.tmp`;
532
+ try {
533
+ fs.writeFileSync(tmp, data);
534
+ fs.renameSync(tmp, filePath);
535
+ } catch (e) {
536
+ try { fs.unlinkSync(tmp); } catch (_) {}
537
+ throw e;
538
+ }
539
+ }
540
+
529
541
  function saveInstallManifest(manifest, customPath = null) {
530
542
  if (DRY_RUN) return;
531
543
  const manifestPath = customPath || getInstallManifestPath();
532
544
  try {
533
545
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
534
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
546
+ writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2));
535
547
  } catch (e) {
536
548
  log.warn(`Could not write install manifest: ${e.message}`);
537
549
  }
@@ -978,7 +990,11 @@ function detectInstallState() {
978
990
  } catch (e) { logDebug(e, 'operation'); }
979
991
  }
980
992
  }
981
- if (!localVersion) localVersion = '3.8.0';
993
+ if (!localVersion) {
994
+ // ponytail: placeholder only feeds the version display/compare; pruning is driven by the file manifest, not this value.
995
+ log.step('Previous AOS version is unknown (no manifest); assuming 3.8.0 for the update comparison only.');
996
+ localVersion = '3.8.0';
997
+ }
982
998
  }
983
999
 
984
1000
  const basePath = scriptDir.includes('_npx') ? path.join(homeDir, '.agents') : path.dirname(srcDir);
@@ -1039,7 +1055,7 @@ function saveManifest(data = {}) {
1039
1055
  platform: process.platform,
1040
1056
  ...data
1041
1057
  });
1042
- fs.writeFileSync(manifestPath, JSON.stringify(updated, null, 2));
1058
+ writeFileAtomic(manifestPath, JSON.stringify(updated, null, 2));
1043
1059
  } catch (e) { logDebug(e, 'manifest read/parse'); }
1044
1060
  }
1045
1061
 
@@ -1218,6 +1234,20 @@ function syncSkillEntry(fullPath, dirName, targetSkillDir, excludeSkills) {
1218
1234
  }
1219
1235
  }
1220
1236
 
1237
+ function redactSecrets(text) {
1238
+ return String(text)
1239
+ .replace(/([\w.-]*(?:key|token|secret|passw(?:or)?d|auth|credential)[\w.-]*\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]')
1240
+ .replace(/\bBearer\s+\S+/gi, 'Bearer [redacted]')
1241
+ .replace(/\b(?:sk|ghp|gho|ghs|xox[a-z])[-_][A-Za-z0-9_-]{16,}/g, '[redacted]');
1242
+ }
1243
+
1244
+ // execFileSync error -> one message that keeps the child's own stderr/stdout (last 2 KB, redacted).
1245
+ function describeExecError(e) {
1246
+ const out = [e && e.stderr, e && e.stdout].map((b) => (b ? b.toString() : '').trim()).filter(Boolean).join('\n');
1247
+ const head = redactSecrets(String((e && e.message) || e).split('\n')[0]).slice(0, 300);
1248
+ return out ? `${head}\n${redactSecrets(out.slice(-2048))}` : head;
1249
+ }
1250
+
1221
1251
  function installStep(what, fn, hint) {
1222
1252
  try {
1223
1253
  return { ok: true, value: fn() };
@@ -1937,7 +1967,7 @@ async function installMemB(interactive) {
1937
1967
  // banner. Stop at the module that could not be fetched.
1938
1968
  if (!downloadOrUpdateModule('@hybridlabor-api/memb', membDir, 'memB Vector Engine')) {
1939
1969
  log.warn('Skipping memB setup: the module could not be downloaded.');
1940
- return;
1970
+ return false;
1941
1971
  }
1942
1972
  await installDeja();
1943
1973
  if (DRY_RUN) {
@@ -1945,6 +1975,7 @@ async function installMemB(interactive) {
1945
1975
  return;
1946
1976
  }
1947
1977
 
1978
+ let membOk = true;
1948
1979
  const reqFile = path.join(membDir, 'requirements.txt');
1949
1980
  const serverPy = path.join(membDir, 'src', 'backend', 'server.py');
1950
1981
  if (fs.existsSync(reqFile)) {
@@ -2017,10 +2048,12 @@ async function installMemB(interactive) {
2017
2048
  );
2018
2049
  if (repaired && !verifyPythonImports(venvPython, membDir, importCheck)) {
2019
2050
  log.warn('memB dependency repair did not pass the import check; inspect the venv before starting the daemon.');
2051
+ membOk = false;
2020
2052
  }
2021
2053
  }
2022
2054
  } catch (e) {
2023
2055
  log.warn(`Failed memB standalone venv setup: ${e.message}`);
2056
+ membOk = false;
2024
2057
  }
2025
2058
  }
2026
2059
 
@@ -2082,13 +2115,14 @@ async function installMemB(interactive) {
2082
2115
  } catch (e) { logDebug(e, 'windows memb daemon setup'); }
2083
2116
  }
2084
2117
  }
2118
+ return membOk;
2085
2119
  }
2086
2120
 
2087
2121
  async function installSynapse() {
2088
2122
  const synapseDir = path.join(moduleBasePath(), 'bdb-synapse');
2089
2123
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-synapse', synapseDir, 'BDB Synapse')) {
2090
2124
  log.warn('Skipping Synapse setup: the module could not be downloaded.');
2091
- return;
2125
+ return false;
2092
2126
  }
2093
2127
  if (DRY_RUN) {
2094
2128
  log.step('[dry-run] would link synapse binary into ~/.local/bin/synapse + setup background daemon');
@@ -2217,6 +2251,7 @@ async function installSynapse() {
2217
2251
  }
2218
2252
  } else {
2219
2253
  log.warn(`No pre-built binary for this platform. Compile with: cd "${synapseDir}" && go build -o ${binaryName} ./cmd/synapse/`);
2254
+ return false;
2220
2255
  }
2221
2256
  }
2222
2257
 
@@ -2410,7 +2445,7 @@ async function installCreatorExtension() {
2410
2445
  const creatorDir = path.join(moduleBasePath(), 'bdb-dev-creator-extension');
2411
2446
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-dev-creator-extension', creatorDir, 'BDB Creator Extension')) {
2412
2447
  log.warn('Skipping Creator Extension setup: the module could not be downloaded.');
2413
- return;
2448
+ return false;
2414
2449
  }
2415
2450
  if (DRY_RUN) {
2416
2451
  log.step('[dry-run] would run BDB Creator Extension setup');
@@ -2423,6 +2458,7 @@ async function installCreatorExtension() {
2423
2458
  : spawnSync('node', [installerScript, '--auto'], { stdio: 'inherit', cwd: creatorDir });
2424
2459
  if (setupResult.status !== 0) {
2425
2460
  log.warn(`Creator Extension setup note: exit code ${setupResult.status}`);
2461
+ return false;
2426
2462
  }
2427
2463
  }
2428
2464
  }
@@ -2431,7 +2467,7 @@ async function installHardwarePcb() {
2431
2467
  const hwDir = path.join(moduleBasePath(), 'bdb-hardware-pcb');
2432
2468
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-hardware-pcb', hwDir, 'BDB Hardware & PCB (KiCad + OpenSCAD)')) {
2433
2469
  log.warn('Skipping Hardware & PCB setup: the module could not be downloaded.');
2434
- return;
2470
+ return false;
2435
2471
  }
2436
2472
  if (DRY_RUN) {
2437
2473
  log.step('[dry-run] would run BDB Hardware & PCB setup');
@@ -2442,6 +2478,7 @@ async function installHardwarePcb() {
2442
2478
  const setupResult = spawnSync('node', [installerScript, '--auto'], { stdio: 'inherit', cwd: hwDir });
2443
2479
  if (setupResult.status !== 0) {
2444
2480
  log.warn(`Hardware & PCB setup note: exit code ${setupResult.status}`);
2481
+ return false;
2445
2482
  }
2446
2483
  }
2447
2484
  }
@@ -2450,7 +2487,7 @@ async function installOSRemoteGateway() {
2450
2487
  const remoteDir = path.join(moduleBasePath(), 'bdb-os-remote');
2451
2488
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-os-remote', remoteDir, 'BDB OS Remote Gateway')) {
2452
2489
  log.warn('Skipping OS Remote Gateway setup: the module could not be downloaded.');
2453
- return;
2490
+ return false;
2454
2491
  }
2455
2492
  }
2456
2493
 
@@ -2458,7 +2495,7 @@ async function installDevToolInstaller() {
2458
2495
  const toolInstallerDir = path.join(moduleBasePath(), 'bdb-dev-tool-installer');
2459
2496
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-dev-tool-installer', toolInstallerDir, 'BDB Dev Tool Installer')) {
2460
2497
  log.warn('Skipping Dev Tool Installer setup: the module could not be downloaded.');
2461
- return;
2498
+ return false;
2462
2499
  }
2463
2500
  }
2464
2501
 
@@ -2470,7 +2507,7 @@ function aoSupportedHere() {
2470
2507
  async function installOSAgentWorkspace() {
2471
2508
  if (!aoSupportedHere()) {
2472
2509
  log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2473
- return;
2510
+ return false;
2474
2511
  }
2475
2512
 
2476
2513
  const isWin = process.platform === 'win32';
@@ -2491,7 +2528,7 @@ async function installOSAgentWorkspace() {
2491
2528
  } else {
2492
2529
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-agent-orchestrator', osAgentDir, 'BDB Agent Orchestrator')) {
2493
2530
  log.warn('Skipping AO setup: the module could not be downloaded.');
2494
- return;
2531
+ return false;
2495
2532
  }
2496
2533
  }
2497
2534
  if (DRY_RUN) {
@@ -2566,7 +2603,7 @@ async function installOSAgentWorkspace() {
2566
2603
 
2567
2604
  if (!daemonBin) {
2568
2605
  log.warn(`AO binary missing in ${osAgentDir} — package layout changed or Go build needed.`);
2569
- return;
2606
+ return false;
2570
2607
  }
2571
2608
 
2572
2609
  if (daemonBin !== binTarget) {
@@ -2602,9 +2639,14 @@ async function installOSAgentWorkspace() {
2602
2639
  }
2603
2640
  }
2604
2641
 
2605
- installStep('register the AO service', () => {
2606
- execFileSync(binTarget, ['service', 'install'], { stdio: 'ignore' });
2642
+ const serviceStep = installStep('register the AO service', () => {
2643
+ try {
2644
+ execFileSync(binTarget, ['service', 'install'], { stdio: ['ignore', 'pipe', 'pipe'] });
2645
+ } catch (e) {
2646
+ throw new Error(describeExecError(e));
2647
+ }
2607
2648
  }, 'Start it by hand with: ao service install');
2649
+ if (!serviceStep.ok) return false;
2608
2650
 
2609
2651
  // Windows startup entries fire asynchronously — give the service more time
2610
2652
  const aoTimeout = process.platform === 'win32' ? 20000 : 8000;
@@ -2660,6 +2702,9 @@ async function promptMemBIngestion(mcpCodeTarget) {
2660
2702
  }
2661
2703
 
2662
2704
  function verifyEcosystemInstallation() {
2705
+ for (const f of moduleFailures) {
2706
+ console.log(` • ${colors.bold}${f.name}${colors.reset} ➔ ${colors.yellow}❌ NOT installed: ${f.reason.split('\n')[0]}${colors.reset}`);
2707
+ }
2663
2708
  const modules = [
2664
2709
  { name: '1. bdb-synapse', pkg: '@hybridlabor-api/bdb-synapse', paths: [path.join(moduleBasePath(), 'bdb-synapse')] },
2665
2710
  { name: '2. memB', pkg: '@hybridlabor-api/memb', paths: [path.join(moduleBasePath(), 'memB'), path.join(geminiDir, 'config', 'mcps', 'memb-mcp')] },
@@ -2839,6 +2884,18 @@ function getTierExcludeSkills(tier) {
2839
2884
  // primary write above owns its file wholesale, but a secondary store belongs
2840
2885
  // to another product and is very likely to already hold the user's own
2841
2886
  // servers -- so this merges per server key and never replaces the file.
2887
+ // An unattended run has no credentials of its own (empty strings); those must never
2888
+ // replace a value the user already has in a server's env block.
2889
+ function keepExistingEnvValues(incomingServers, previousServers) {
2890
+ for (const [key, incoming] of Object.entries(incomingServers || {})) {
2891
+ const prevEnv = previousServers && previousServers[key] && previousServers[key].env;
2892
+ if (!incoming || typeof incoming !== 'object' || !incoming.env || !prevEnv || typeof prevEnv !== 'object') continue;
2893
+ for (const [k, v] of Object.entries(incoming.env)) {
2894
+ if (v === '' && typeof prevEnv[k] === 'string' && prevEnv[k] !== '') incoming.env[k] = prevEnv[k];
2895
+ }
2896
+ }
2897
+ }
2898
+
2842
2899
  function mirrorMcpServersTo(extraPaths, mcpConfigStr) {
2843
2900
  if (!Array.isArray(extraPaths) || extraPaths.length === 0) return;
2844
2901
 
@@ -2862,7 +2919,9 @@ function mirrorMcpServersTo(extraPaths, mcpConfigStr) {
2862
2919
  }
2863
2920
  const data = existing || {};
2864
2921
  data.mcpServers = data.mcpServers && typeof data.mcpServers === 'object' ? data.mcpServers : {};
2865
- for (const [key, val] of Object.entries(servers)) data.mcpServers[key] = val;
2922
+ const mirrored = JSON.parse(JSON.stringify(servers));
2923
+ keepExistingEnvValues(mirrored, data.mcpServers);
2924
+ for (const [key, val] of Object.entries(mirrored)) data.mcpServers[key] = val;
2866
2925
  fs.mkdirSync(path.dirname(target), { recursive: true });
2867
2926
  // Carries injected API keys, same as the primary config. The `mode`
2868
2927
  // option only applies when writeFileSync CREATES the file -- an
@@ -3182,6 +3241,7 @@ async function installMcpsForTarget(paths, ctx) {
3182
3241
  if (!incoming || typeof incoming !== 'object') return;
3183
3242
  if (!incoming.env && previous.env) incoming.env = previous.env;
3184
3243
  });
3244
+ keepExistingEnvValues(newServers, oldServers);
3185
3245
  oldConfig.mcpServers = Object.assign({}, oldServers, newServers);
3186
3246
  fs.writeFileSync(paths.mcpConfigPath, JSON.stringify(oldConfig, null, 2), { mode: 0o600 });
3187
3247
  try { fs.chmodSync(paths.mcpConfigPath, 0o600); } catch (e) { logDebug(e, 'chmod mcpConfigPath'); }
@@ -3920,9 +3980,11 @@ function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3920
3980
  .join(' ');
3921
3981
  return bdbHookScripts.some((name) => cmds.includes(name));
3922
3982
  };
3923
- const localize = (cmd) => (projectLocal && !machineGlobalHooks.some((n) => cmd.includes(n))
3924
- ? cmd.split('${HOME}').join('$CLAUDE_PROJECT_DIR')
3925
- : cmd);
3983
+ // Global installs must point at the copies installGlobalHooks put under
3984
+ // $HOME/.claude/hooks; $CLAUDE_PROJECT_DIR would not exist in other projects.
3985
+ const localize = (cmd) => (projectLocal
3986
+ ? (machineGlobalHooks.some((n) => cmd.includes(n)) ? cmd : cmd.split('${HOME}').join('$CLAUDE_PROJECT_DIR'))
3987
+ : cmd.split('$CLAUDE_PROJECT_DIR/.claude/hooks/').join('$HOME/.claude/hooks/'));
3926
3988
  const cloneBdbEntries = (entries) =>
3927
3989
  JSON.parse(JSON.stringify(entries)).map((e) => ({
3928
3990
  ...e,
@@ -3969,7 +4031,16 @@ function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3969
4031
 
3970
4032
  try {
3971
4033
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
3972
- fs.writeFileSync(settingsPath, JSON.stringify(buildMerged(existing), null, 2) + '\n');
4034
+ const tmpPath = `${settingsPath}.${process.pid}.tmp`;
4035
+ if (existing) {
4036
+ fs.copyFileSync(settingsPath, `${settingsPath}.${process.hrtime.bigint()}.bak`);
4037
+ const dir = path.dirname(settingsPath);
4038
+ const base = path.basename(settingsPath);
4039
+ const baks = fs.readdirSync(dir).filter((f) => f.startsWith(`${base}.`) && /\.\d{13,}\.bak$/.test(f)).sort();
4040
+ for (const old of baks.slice(0, -3)) fs.rmSync(path.join(dir, old), { force: true });
4041
+ }
4042
+ fs.writeFileSync(tmpPath, JSON.stringify(buildMerged(existing), null, 2) + '\n', { mode: existing ? fs.statSync(settingsPath).mode & 0o777 : 0o644 });
4043
+ fs.renameSync(tmpPath, settingsPath);
3973
4044
  } catch (e) {
3974
4045
  log.warn(`Could not write ${settingsPath}: ${e.message}`);
3975
4046
  }
@@ -4178,13 +4249,35 @@ function mergeCodexTomlHooks(configTomlPath, { projectLocal = false } = {}) {
4178
4249
  }
4179
4250
  }
4180
4251
 
4181
- // Replace existing AOS:HOOKS block or append
4182
- const hookBlockRegex = /# AOS:HOOKS:START[\s\S]*?# AOS:HOOKS:END/;
4183
- if (hookBlockRegex.test(content)) {
4184
- content = content.replace(hookBlockRegex, tomlSnippet);
4185
- } else {
4186
- content = `${content.trimEnd()}\n\n${tomlSnippet}\n`;
4187
- }
4252
+ // Drop every AOS hook entry (marker block or not, duplicates included), then append one fresh block.
4253
+ const bdbCodexScripts = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'startcycle-dispatch.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
4254
+ const isAosMarker = (l) => /^\s*# AOS:HOOKS:(START|END)\s*$/.test(l);
4255
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
4256
+ const lines = content.split(/\r?\n/).filter((l) => !isAosMarker(l));
4257
+ const sections = [];
4258
+ for (const l of lines) {
4259
+ if (/^\s*\[/.test(l) || !sections.length) sections.push([l]);
4260
+ else sections[sections.length - 1].push(l);
4261
+ }
4262
+ const isHookHeader = (sec) => /^\s*\[\[hooks\.\w+\]\]\s*$/.test(sec[0]);
4263
+ const isHookInner = (sec) => /^\s*\[\[hooks\.\w+\.hooks\]\]\s*$/.test(sec[0]);
4264
+ const kept = [];
4265
+ for (let i = 0; i < sections.length; i++) {
4266
+ if (!isHookHeader(sections[i])) { kept.push(sections[i]); continue; }
4267
+ let j = i + 1;
4268
+ while (j < sections.length && isHookInner(sections[j])) j++;
4269
+ const group = sections.slice(i, j);
4270
+ const ours = group.some((sec) => sec.some((l) => /^\s*command\s*=/.test(l) && bdbCodexScripts.some((n) => l.includes(n))));
4271
+ if (ours) {
4272
+ // keep trailing blanks/comments (they may belong to another tool's marker)
4273
+ const tail = group.flat();
4274
+ let end = tail.length;
4275
+ while (end > 0 && /^\s*(#.*)?$/.test(tail[end - 1])) end--;
4276
+ if (end < tail.length) kept.push(tail.slice(end));
4277
+ } else kept.push(...group);
4278
+ i = j - 1;
4279
+ }
4280
+ content = `${kept.flat().join(eol).trimEnd()}${eol}${eol}${tomlSnippet.split('\n').join(eol)}${eol}`;
4188
4281
 
4189
4282
  try {
4190
4283
  fs.mkdirSync(path.dirname(configTomlPath), { recursive: true });
@@ -4350,13 +4443,33 @@ async function promptOptionalModules(installedModules) {
4350
4443
  required: false
4351
4444
  }));
4352
4445
 
4446
+ return (await runModuleInstalls(allModules, chosen, installedModules)).installed;
4447
+ }
4448
+
4449
+ // Module installers return false (or throw) on failure; undefined/true mean success.
4450
+ // Only successes are recorded, so the manifest never claims a module that is not there.
4451
+ const moduleFailures = [];
4452
+ async function runModuleInstalls(allModules, chosen, installedModules) {
4453
+ const installed = [];
4454
+ const failed = [];
4353
4455
  for (const mod of allModules) {
4354
- if (chosen.includes(mod.id)) {
4355
- await mod.fn();
4356
- installedModules.push(mod.id);
4456
+ if (!chosen.includes(mod.id)) continue;
4457
+ let reason = null;
4458
+ try {
4459
+ if (await mod.fn() === false) reason = 'installation reported a failure (see warnings above)';
4460
+ } catch (e) {
4461
+ reason = e && e.message ? e.message : String(e);
4462
+ }
4463
+ if (reason === null) {
4464
+ installed.push(mod.id);
4465
+ if (!installedModules.includes(mod.id)) installedModules.push(mod.id);
4466
+ } else {
4467
+ failed.push({ id: mod.id, name: mod.name, reason });
4468
+ moduleFailures.push({ id: mod.id, name: mod.name, reason });
4469
+ log.warn(`${mod.name} was not installed: ${reason}`);
4357
4470
  }
4358
4471
  }
4359
- return chosen;
4472
+ return { installed, failed };
4360
4473
  }
4361
4474
 
4362
4475
  // Module ids whose daemons ship a web interface or background service worth
@@ -5586,6 +5699,9 @@ module.exports = {
5586
5699
  downloadOrUpdateModule,
5587
5700
  detectPlatforms,
5588
5701
  markPlatformsExplicit,
5702
+ runModuleInstalls,
5703
+ describeExecError,
5704
+ keepExistingEnvValues,
5589
5705
  mergeBdbSettingsHooks,
5590
5706
  mergeAntigravityHooks,
5591
5707
  mergeCodexTomlHooks,
@@ -83,7 +83,7 @@ function act(i){
83
83
  }
84
84
  function row(i){
85
85
  const k=key(i),open=S.expanded.has(k);
86
- const badge=(i.fileCount>1?`<span class="badge">${i.fileCount} files</span>`:'')+(i.execBridge?'<span class="badge warn">exec bridge</span>':'')+(i.requires.length?`<span class="badge">+${i.requires.length} required</span>`:'');
86
+ const badge=(i.fileCount>1?`<span class="badge">${i.fileCount} files</span>`:'')+(i.execBridge?'<span class="badge warn">exec bridge</span>':'')+((i.requires||[]).length?`<span class="badge">+${i.requires.length} required</span>`:'');
87
87
  let h=`<div class="rowwrap"><div class="row"><span class="dot ${i.installed?'g':''}"></span><span class="n" data-a="exp" data-v="${esc(k)}" style="cursor:pointer" title="Show description">${esc(i.name)}${badge}</span><span class="d" style="color:var(--muted)">${esc(i.description)}</span><span class="c cat">${esc(i.category)}</span><span class="s src">${esc(i.source)}</span><span style="text-align:right">${act(i)}</span></div>`;
88
88
  if(open){const d=i.core?i.description:S.detail[k];h+=`<div class="detail">${d===undefined?'Loading...':esc(d)}</div>`}
89
89
  return h+'</div>';
@@ -24,7 +24,7 @@ function catalog(scope, cwd) {
24
24
  const core = { skill: localSkills(ROOT), agent: localAgents(ROOT) };
25
25
  for (const kind of ['skill', 'agent']) {
26
26
  for (const [name, file] of core[kind]) {
27
- items.push({ name, kind, description: frontDescription(file), category: 'core', source: 'AOS Core', installed: true, core: true });
27
+ items.push({ name, kind, description: frontDescription(file), category: 'core', source: 'AOS Core', installed: true, core: true, requires: [] });
28
28
  }
29
29
  }
30
30
  for (const src of sources) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridlabor-api/aos",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
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": {
@@ -27,7 +27,7 @@
27
27
  "plugin:build": "node scripts/build-plugin-manifest.mjs",
28
28
  "plugin:check": "node scripts/build-plugin-manifest.mjs --check",
29
29
  "doctor": "node bin/aos-doctor.mjs",
30
- "test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node --test tests/aos-store-ui.test.mjs && node tests/aos-store-scenario.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs && node --test tests/agent-models.test.js"
30
+ "test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node --test tests/aos-store-ui.test.mjs && node tests/aos-store-scenario.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs && node --test tests/agent-models.test.js && node --test tests/installer-p3.test.js"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -58,8 +58,7 @@ Deletes a specific memory segment using its UUID.
58
58
 
59
59
  ## 🔒 Security Hardening
60
60
 
61
- The `memb-mcp` server runs a pre-ingestion regex filter that blocks or redacts standard credential patterns:
62
- * API keys (Google Cloud, OpenAI, GitHub, etc.)
63
- * Plaintext passwords
64
- * Database URLs
65
- This safeguards your workspace metadata and prevents credentials from leaking into vector repositories.
61
+ The `memb-mcp` server has **no automatic credential filter** today: it stores whatever an agent sends it.
62
+ * Never ingest credentials: API keys (Google Cloud, OpenAI, GitHub, etc.), plaintext passwords, database URLs.
63
+ * The protection is the rule in this skill (and in `memb-skill`) plus human review of stored memories.
64
+ * An automatic pre-ingestion filter is planned but not released.
@@ -28,16 +28,13 @@ AOS from [affaan-m/ECC](https://github.com/affaan-m/ECC).
28
28
  decision — the canvas verdict replaces a typed "yes/proceed".
29
29
  - **Mandatory, not optional**, at the end of `bdbrainstorm` (before writing
30
30
  `state.goal` / handing off to `/startcycle-graph`) and `bdbmediastorm`
31
- - **Visual Extensions (BuilderIO Integration):** When generating plans, always ask the human which Canvas version they prefer:
32
- - **Plan-Canvas Preview**: Standard markdown/html review.
33
- - **Archify Canvas**: For strict architecture schema validation.
34
- - **Visual Ecosystem**: Use `/visual-plan` (turn text plans into rich visual plans), `/visual-recap` (turn diffs into interactive visual recaps), or `/visual-edit` (open a running local app for visual editing).
35
31
  (before the show-control architecture is considered final) — both produce
36
32
  a plan/spec artifact a human must approve before anything downstream
37
33
  proceeds. See each skill's own "Plan Canvas Review" section.
38
34
  - The user should *point at* what to change: reviewing designs, comparisons,
39
35
  reports, or any local `.md` / `.html` artifact.
40
36
  - The user asks for a visual review, or "open it in the browser".
37
+ - The artifact is an Archify diagram (the standalone HTML that `archify` produces): `.html` artifacts render as-is with the annotation layer, so architecture reviews use the same loop.
41
38
 
42
39
  This tool is a plain Node CLI speaking JSON over a loopback HTTP server —
43
40
  it has no dependency on which agent harness invokes it (Claude Code, Codex,