@hybridlabor-api/aos 4.4.0 → 4.4.1
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.
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// aos-hook-version: 2
|
|
2
3
|
/**
|
|
3
4
|
* memB ambient memory hook for Claude Code (UserPromptSubmit).
|
|
4
5
|
*
|
|
6
|
+
* The version line above is what /aos-setup's doctor reads to tell a current
|
|
7
|
+
* hook from a stale one left behind by an older install. Bump it whenever this
|
|
8
|
+
* file changes in a way a machine must actually pick up, and bump the expected
|
|
9
|
+
* value in aos-doctor.mjs in the same commit.
|
|
10
|
+
*
|
|
5
11
|
* Reads ~/.MemBDB/memb.db directly through node:sqlite and injects the most
|
|
6
12
|
* relevant memories as ephemeral additionalContext — nothing is written to
|
|
7
13
|
* disk and nothing lands in git. Fails open: any error exits 0 silently, so a
|
package/installer.js
CHANGED
|
@@ -508,10 +508,30 @@ function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes
|
|
|
508
508
|
|
|
509
509
|
const diskHash = computeFileHash(targetPath);
|
|
510
510
|
|
|
511
|
-
// Case:
|
|
511
|
+
// Case: unrecognised content at a path this payload owns.
|
|
512
|
+
//
|
|
513
|
+
// This used to skip, on the reading that unknown content means a file the
|
|
514
|
+
// installer did not place. But resolveFileConflict only ever sees paths the
|
|
515
|
+
// payload is actively writing, so the path IS ours -- and the commonest
|
|
516
|
+
// cause of unknown content is simply an older release of our own file,
|
|
517
|
+
// installed before manifest tracking existed and therefore never adopted.
|
|
518
|
+
// Skipping froze those permanently: v4.4.0 still found skills on disk
|
|
519
|
+
// carrying pre-reorg ~/bdb-dev paths and a stale `category:`, untouched
|
|
520
|
+
// across every update since, because bootstrap adoption only catches a file
|
|
521
|
+
// that is byte-identical to the current payload.
|
|
522
|
+
//
|
|
523
|
+
// So update it, and keep the protection where it belongs -- in the backup,
|
|
524
|
+
// not in refusing to write. A genuine local edit is recoverable from .bak
|
|
525
|
+
// rather than silently outvoting the shipped version forever.
|
|
512
526
|
if (!manifestEntry && !knownSourceHashes.has(diskHash)) {
|
|
513
|
-
|
|
514
|
-
|
|
527
|
+
const bakPath = `${targetPath}.bak`;
|
|
528
|
+
try { fs.copyFileSync(targetPath, bakPath); } catch (e) {
|
|
529
|
+
log.warn(`[manifest] Could not create backup ${bakPath}: ${e.message}`);
|
|
530
|
+
}
|
|
531
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
532
|
+
manifest[targetPath] = { path: targetPath, sha256: sourceHash, version: pkg.version, installedAt: new Date().toISOString() };
|
|
533
|
+
log.warn(`[manifest] Unrecognised ${path.basename(targetPath)} backed up to ${bakPath}, shipped version written.`);
|
|
534
|
+
return 'bak';
|
|
515
535
|
}
|
|
516
536
|
|
|
517
537
|
// Case: first-run bootstrap – disk hash matches source hash but no manifest entry.
|
|
@@ -2717,7 +2737,7 @@ function injectHarnessRules() {
|
|
|
2717
2737
|
log.step(`Copied ${dir} to ${targetPath}`);
|
|
2718
2738
|
}
|
|
2719
2739
|
});
|
|
2720
|
-
|
|
2740
|
+
installGlobalHooks();
|
|
2721
2741
|
}, '');
|
|
2722
2742
|
|
|
2723
2743
|
installStep('sync global .agents/', () => {
|
|
@@ -2731,6 +2751,22 @@ function injectHarnessRules() {
|
|
|
2731
2751
|
}
|
|
2732
2752
|
}
|
|
2733
2753
|
|
|
2754
|
+
// Deliver the hook scripts to ~/.claude/hooks/ and wire them in settings.json.
|
|
2755
|
+
// Both the full install and Quick Update funnel through here. Quick Update used
|
|
2756
|
+
// to do neither: it refreshes skills and submodules, but hooks are harness
|
|
2757
|
+
// plumbing rather than skills, so an existing install updating to a version
|
|
2758
|
+
// that adds or changes a hook silently never received it -- which is exactly
|
|
2759
|
+
// what happened to memb-inject.mjs in v4.4.0, on the machines that needed it
|
|
2760
|
+
// most (a fresh install got it; every machine that already had AOS did not).
|
|
2761
|
+
function installGlobalHooks() {
|
|
2762
|
+
const hooksSrc = path.join(srcDir, '.claude', 'hooks');
|
|
2763
|
+
if (fs.existsSync(hooksSrc)) {
|
|
2764
|
+
copyDirRecursiveSync(hooksSrc, path.join(homeDir, '.claude', 'hooks'));
|
|
2765
|
+
log.step(`Installed hooks to ${path.join(homeDir, '.claude', 'hooks')}`);
|
|
2766
|
+
}
|
|
2767
|
+
mergeBdbSettingsHooks(path.join(homeDir, '.claude', 'settings.json'));
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2734
2770
|
// Merge the BDB hooks -- the two gates plus the memB ambient-memory hook --
|
|
2735
2771
|
// into a Claude Code settings.json without clobbering
|
|
2736
2772
|
// user-owned keys (v3.13 audit BLOCKER-3: the harness-dir copy used to
|
|
@@ -3298,6 +3334,10 @@ async function runQuickUpdate(installState) {
|
|
|
3298
3334
|
syncSkillsToGlobalHarnesses(excludeSkills);
|
|
3299
3335
|
s.stop('Skills refreshed');
|
|
3300
3336
|
|
|
3337
|
+
installStep('refresh gate + memory hooks', () => {
|
|
3338
|
+
installGlobalHooks();
|
|
3339
|
+
}, 'Hooks keep whatever version this machine already had.');
|
|
3340
|
+
|
|
3301
3341
|
// OpenWiki setup only ever ran on a brand-new install (main()'s fresh-install
|
|
3302
3342
|
// branch below) -- an existing install running Quick Update never got offered
|
|
3303
3343
|
// it and never had its daemon schedule refreshed. promptCredentials() already
|
|
@@ -3728,6 +3768,7 @@ if (require.main === module) {
|
|
|
3728
3768
|
// Exported for tests -- requiring installer.js must not launch the TUI.
|
|
3729
3769
|
module.exports = {
|
|
3730
3770
|
mergeBdbSettingsHooks,
|
|
3771
|
+
installGlobalHooks,
|
|
3731
3772
|
installProjectHarness,
|
|
3732
3773
|
mirrorMcpServersTo,
|
|
3733
3774
|
// Manifest store (exported for verification tests)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybridlabor-api/aos",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.1",
|
|
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
|
"bin": {
|
|
@@ -123,9 +123,11 @@ injects the relevant memories as context. It fails open: any error exits `0`
|
|
|
123
123
|
and the prompt proceeds untouched.
|
|
124
124
|
|
|
125
125
|
Since v4.4.0 the installer ships `memb-inject.mjs` into `~/.claude/hooks/` and
|
|
126
|
-
wires it as a `UserPromptSubmit` hook
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
wires it as a `UserPromptSubmit` hook. **v4.4.0's Quick Update did not** — it
|
|
127
|
+
refreshes skills and submodules, and hooks are harness plumbing rather than
|
|
128
|
+
skills, so a machine that already had AOS updated to 4.4.0 without ever
|
|
129
|
+
receiving the hook. Fixed in v4.4.1; a machine that took that update needs one
|
|
130
|
+
more run:
|
|
129
131
|
|
|
130
132
|
```bash
|
|
131
133
|
npx -y @hybridlabor-api/aos@latest
|
|
@@ -135,6 +137,13 @@ The wiring is merged into `~/.claude/settings.json`, never written over it:
|
|
|
135
137
|
user keys and foreign hook entries survive, and a re-run replaces the BDB entry
|
|
136
138
|
rather than adding a second copy.
|
|
137
139
|
|
|
140
|
+
The hook carries an `aos-hook-version:` line on its second line, and the doctor
|
|
141
|
+
compares it against what the installed release expects. That is why the row can
|
|
142
|
+
read *stale* rather than simply passing: a hook file that exists and is wired
|
|
143
|
+
can still be an old copy carrying a bug this version fixed. When you change the
|
|
144
|
+
hook in a way machines must pick up, bump that line and the expected value in
|
|
145
|
+
`aos-doctor.mjs` in the same commit — the test suite fails if the two disagree.
|
|
146
|
+
|
|
138
147
|
Unlike the two gate hooks, this one stays `$HOME`-anchored even in a project
|
|
139
148
|
harness — the memB store is machine-global, and pointing it at
|
|
140
149
|
`$CLAUDE_PROJECT_DIR` would make it fail on every prompt in any project the
|
|
@@ -139,10 +139,35 @@ function checkAos() {
|
|
|
139
139
|
// ---------------------------------------------------------------- hooks
|
|
140
140
|
function checkHooks() {
|
|
141
141
|
const hooksDir = h('.claude', 'hooks');
|
|
142
|
+
|
|
143
|
+
// Existence is not enough: an older install leaves a stale copy behind, and
|
|
144
|
+
// the row would go green over a hook carrying a bug this version fixed.
|
|
145
|
+
// Hooks that carry an `aos-hook-version:` line are checked against what this
|
|
146
|
+
// release expects; the ones that do not are existence-only.
|
|
147
|
+
const EXPECTED_VERSION = { 'memb-inject.mjs': 2 };
|
|
148
|
+
const versionOf = (text) => {
|
|
149
|
+
const m = /^\/\/\s*aos-hook-version:\s*(\d+)/m.exec(text);
|
|
150
|
+
return m ? Number(m[1]) : null;
|
|
151
|
+
};
|
|
152
|
+
|
|
142
153
|
for (const file of ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs']) {
|
|
143
154
|
const p = path.join(hooksDir, file);
|
|
144
|
-
|
|
145
|
-
'
|
|
155
|
+
if (!existsSync(p)) {
|
|
156
|
+
add('hooks', file, false, 'missing',
|
|
157
|
+
'npx -y @hybridlabor-api/aos@latest (the installer ships and wires all three).');
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const want = EXPECTED_VERSION[file];
|
|
161
|
+
if (!want) {
|
|
162
|
+
add('hooks', file, true, tilde(p), '');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const got = versionOf(readFileSync(p, 'utf8'));
|
|
166
|
+
add('hooks', file, got === want,
|
|
167
|
+
got === want ? `${tilde(p)} (v${got})`
|
|
168
|
+
: got === null ? `${tilde(p)} — predates version stamping, so it is older than v${want}`
|
|
169
|
+
: `${tilde(p)} — v${got}, this release ships v${want}`,
|
|
170
|
+
'npx -y @hybridlabor-api/aos@latest — Quick Update refreshes hooks from v4.4.1 on; an older installer never replaced them.');
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
const settings = readJson(h('.claude', 'settings.json'));
|