hebbian 0.3.2 → 0.3.4
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/dist/bin/hebbian.js +616 -117
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/digest.d.ts +1 -1
- package/dist/digest.d.ts.map +1 -1
- package/dist/emit.d.ts.map +1 -1
- package/dist/evolve.d.ts +22 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/grow.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +307 -20
- package/dist/index.js.map +1 -1
- package/dist/update-check.d.ts +16 -0
- package/dist/update-check.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/bin/hebbian.js
CHANGED
|
@@ -555,6 +555,8 @@ function writeTarget(filePath, content) {
|
|
|
555
555
|
writeFileSync2(filePath, before + content + after, "utf8");
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
558
|
+
writeFileSync2(filePath, content + "\n\n" + existing, "utf8");
|
|
559
|
+
return;
|
|
558
560
|
}
|
|
559
561
|
writeFileSync2(filePath, content, "utf8");
|
|
560
562
|
}
|
|
@@ -607,28 +609,174 @@ var init_emit = __esm({
|
|
|
607
609
|
}
|
|
608
610
|
});
|
|
609
611
|
|
|
612
|
+
// src/update-check.ts
|
|
613
|
+
var update_check_exports = {};
|
|
614
|
+
__export(update_check_exports, {
|
|
615
|
+
checkForUpdates: () => checkForUpdates,
|
|
616
|
+
formatUpdateBanner: () => formatUpdateBanner
|
|
617
|
+
});
|
|
618
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, statSync as statSync2, unlinkSync } from "fs";
|
|
619
|
+
import { join as join4 } from "path";
|
|
620
|
+
function getStateDir() {
|
|
621
|
+
return join4(process.env.HOME || "~", ".hebbian");
|
|
622
|
+
}
|
|
623
|
+
function ensureStateDir(stateDir) {
|
|
624
|
+
if (!existsSync5(stateDir)) {
|
|
625
|
+
mkdirSync3(stateDir, { recursive: true });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function isCacheStale(cachePath, type) {
|
|
629
|
+
try {
|
|
630
|
+
const mtime = statSync2(cachePath).mtimeMs;
|
|
631
|
+
const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
|
|
632
|
+
const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
|
|
633
|
+
return ageMinutes > ttl;
|
|
634
|
+
} catch {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function readCache(stateDir) {
|
|
639
|
+
const cachePath = join4(stateDir, "last-update-check");
|
|
640
|
+
if (!existsSync5(cachePath)) return null;
|
|
641
|
+
try {
|
|
642
|
+
const line = readFileSync3(cachePath, "utf8").trim();
|
|
643
|
+
if (line.startsWith("UP_TO_DATE")) {
|
|
644
|
+
if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
|
|
645
|
+
const ver = line.split(/\s+/)[1];
|
|
646
|
+
return { type: "UP_TO_DATE", current: ver };
|
|
647
|
+
}
|
|
648
|
+
if (line.startsWith("UPGRADE_AVAILABLE")) {
|
|
649
|
+
if (isCacheStale(cachePath, "UPGRADE_AVAILABLE")) return null;
|
|
650
|
+
const [, current, latest] = line.split(/\s+/);
|
|
651
|
+
return { type: "UPGRADE_AVAILABLE", current, latest };
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
} catch {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function writeCache(stateDir, line) {
|
|
659
|
+
ensureStateDir(stateDir);
|
|
660
|
+
writeFileSync3(join4(stateDir, "last-update-check"), line, "utf8");
|
|
661
|
+
}
|
|
662
|
+
function isSnoozed(stateDir, remoteVersion) {
|
|
663
|
+
const snoozePath = join4(stateDir, "update-snoozed");
|
|
664
|
+
if (!existsSync5(snoozePath)) return false;
|
|
665
|
+
try {
|
|
666
|
+
const [ver, levelStr, epochStr] = readFileSync3(snoozePath, "utf8").trim().split(/\s+/);
|
|
667
|
+
if (ver !== remoteVersion) {
|
|
668
|
+
unlinkSync(snoozePath);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
const level = parseInt(levelStr || "1", 10);
|
|
672
|
+
const epoch = parseInt(epochStr || "0", 10);
|
|
673
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
674
|
+
const duration = SNOOZE_DURATIONS[Math.min(level, 3)] ?? SNOOZE_DURATIONS[3];
|
|
675
|
+
return now < epoch + duration;
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function fetchLatestVersion() {
|
|
681
|
+
try {
|
|
682
|
+
const controller = new AbortController();
|
|
683
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
684
|
+
const res = await fetch(NPM_REGISTRY_URL, {
|
|
685
|
+
signal: controller.signal,
|
|
686
|
+
headers: { Accept: "application/json" }
|
|
687
|
+
});
|
|
688
|
+
clearTimeout(timeout);
|
|
689
|
+
if (!res.ok) return null;
|
|
690
|
+
const data = await res.json();
|
|
691
|
+
const version = data.version;
|
|
692
|
+
if (!version || !/^\d+\.\d+[\d.]*$/.test(version)) return null;
|
|
693
|
+
return version;
|
|
694
|
+
} catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async function checkForUpdates(currentVersion) {
|
|
699
|
+
if (process.env.HEBBIAN_UPDATE_CHECK === "false") {
|
|
700
|
+
return { type: "skipped" };
|
|
701
|
+
}
|
|
702
|
+
const stateDir = getStateDir();
|
|
703
|
+
const cached = readCache(stateDir);
|
|
704
|
+
if (cached) {
|
|
705
|
+
if (cached.type === "UP_TO_DATE") {
|
|
706
|
+
return { type: "up_to_date" };
|
|
707
|
+
}
|
|
708
|
+
if (cached.type === "UPGRADE_AVAILABLE" && cached.current && cached.latest) {
|
|
709
|
+
if (cached.current === currentVersion && !isSnoozed(stateDir, cached.latest)) {
|
|
710
|
+
return { type: "upgrade_available", current: currentVersion, latest: cached.latest };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const latest = await fetchLatestVersion();
|
|
715
|
+
if (!latest) {
|
|
716
|
+
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
717
|
+
return { type: "up_to_date" };
|
|
718
|
+
}
|
|
719
|
+
if (latest === currentVersion) {
|
|
720
|
+
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
721
|
+
return { type: "up_to_date" };
|
|
722
|
+
}
|
|
723
|
+
writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
|
|
724
|
+
if (isSnoozed(stateDir, latest)) {
|
|
725
|
+
return { type: "up_to_date" };
|
|
726
|
+
}
|
|
727
|
+
return { type: "upgrade_available", current: currentVersion, latest };
|
|
728
|
+
}
|
|
729
|
+
function formatUpdateBanner(status) {
|
|
730
|
+
if (status.type !== "upgrade_available") return null;
|
|
731
|
+
return [
|
|
732
|
+
``,
|
|
733
|
+
` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
|
|
734
|
+
` npm i -g hebbian@latest`,
|
|
735
|
+
``
|
|
736
|
+
].join("\n");
|
|
737
|
+
}
|
|
738
|
+
var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
|
|
739
|
+
var init_update_check = __esm({
|
|
740
|
+
"src/update-check.ts"() {
|
|
741
|
+
"use strict";
|
|
742
|
+
PACKAGE_NAME = "hebbian";
|
|
743
|
+
NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
744
|
+
FETCH_TIMEOUT_MS = 5e3;
|
|
745
|
+
TTL_UP_TO_DATE = 60;
|
|
746
|
+
TTL_UPGRADE_AVAILABLE = 720;
|
|
747
|
+
SNOOZE_DURATIONS = {
|
|
748
|
+
1: 86400,
|
|
749
|
+
// 24h
|
|
750
|
+
2: 172800,
|
|
751
|
+
// 48h
|
|
752
|
+
3: 604800
|
|
753
|
+
// 7d (and beyond)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
610
758
|
// src/fire.ts
|
|
611
759
|
var fire_exports = {};
|
|
612
760
|
__export(fire_exports, {
|
|
613
761
|
fireNeuron: () => fireNeuron,
|
|
614
762
|
getCurrentCounter: () => getCurrentCounter
|
|
615
763
|
});
|
|
616
|
-
import { readdirSync as readdirSync3, renameSync, writeFileSync as
|
|
617
|
-
import { join as
|
|
764
|
+
import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
765
|
+
import { join as join5 } from "path";
|
|
618
766
|
function fireNeuron(brainRoot, neuronPath) {
|
|
619
|
-
const fullPath =
|
|
620
|
-
if (!
|
|
621
|
-
|
|
622
|
-
|
|
767
|
+
const fullPath = join5(brainRoot, neuronPath);
|
|
768
|
+
if (!existsSync6(fullPath)) {
|
|
769
|
+
mkdirSync4(fullPath, { recursive: true });
|
|
770
|
+
writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
|
|
623
771
|
console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
|
|
624
772
|
return 1;
|
|
625
773
|
}
|
|
626
774
|
const current = getCurrentCounter(fullPath);
|
|
627
775
|
const newCounter = current + 1;
|
|
628
776
|
if (current > 0) {
|
|
629
|
-
renameSync(
|
|
777
|
+
renameSync(join5(fullPath, `${current}.neuron`), join5(fullPath, `${newCounter}.neuron`));
|
|
630
778
|
} else {
|
|
631
|
-
|
|
779
|
+
writeFileSync4(join5(fullPath, `${newCounter}.neuron`), "", "utf8");
|
|
632
780
|
}
|
|
633
781
|
console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
634
782
|
return newCounter;
|
|
@@ -654,7 +802,7 @@ var init_fire = __esm({
|
|
|
654
802
|
|
|
655
803
|
// src/similarity.ts
|
|
656
804
|
function tokenize(name) {
|
|
657
|
-
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[
|
|
805
|
+
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
|
|
658
806
|
}
|
|
659
807
|
function stem(word) {
|
|
660
808
|
const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
|
|
@@ -688,11 +836,11 @@ var grow_exports = {};
|
|
|
688
836
|
__export(grow_exports, {
|
|
689
837
|
growNeuron: () => growNeuron
|
|
690
838
|
});
|
|
691
|
-
import { mkdirSync as
|
|
692
|
-
import { join as
|
|
839
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
|
|
840
|
+
import { join as join6, relative as relative2 } from "path";
|
|
693
841
|
function growNeuron(brainRoot, neuronPath) {
|
|
694
|
-
const fullPath =
|
|
695
|
-
if (
|
|
842
|
+
const fullPath = join6(brainRoot, neuronPath);
|
|
843
|
+
if (existsSync7(fullPath)) {
|
|
696
844
|
const counter = fireNeuron(brainRoot, neuronPath);
|
|
697
845
|
return { action: "fired", path: neuronPath, counter };
|
|
698
846
|
}
|
|
@@ -702,10 +850,12 @@ function growNeuron(brainRoot, neuronPath) {
|
|
|
702
850
|
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}`);
|
|
703
851
|
}
|
|
704
852
|
const leafName = parts[parts.length - 1];
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
853
|
+
const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
854
|
+
const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
855
|
+
const newTokens = tokenize(newStripped);
|
|
856
|
+
const regionPath = join6(brainRoot, regionName);
|
|
857
|
+
if (existsSync7(regionPath)) {
|
|
858
|
+
const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
|
|
709
859
|
if (match) {
|
|
710
860
|
const matchRelPath = regionName + "/" + relative2(regionPath, match);
|
|
711
861
|
console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
|
|
@@ -713,12 +863,12 @@ function growNeuron(brainRoot, neuronPath) {
|
|
|
713
863
|
return { action: "fired", path: matchRelPath, counter };
|
|
714
864
|
}
|
|
715
865
|
}
|
|
716
|
-
|
|
717
|
-
|
|
866
|
+
mkdirSync5(fullPath, { recursive: true });
|
|
867
|
+
writeFileSync5(join6(fullPath, "1.neuron"), "", "utf8");
|
|
718
868
|
console.log(`\u{1F331} grew: ${neuronPath} (1)`);
|
|
719
869
|
return { action: "grew", path: neuronPath, counter: 1 };
|
|
720
870
|
}
|
|
721
|
-
function findSimilar(dir, regionRoot, targetTokens) {
|
|
871
|
+
function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
|
|
722
872
|
let entries;
|
|
723
873
|
try {
|
|
724
874
|
entries = readdirSync4(dir, { withFileTypes: true });
|
|
@@ -728,16 +878,19 @@ function findSimilar(dir, regionRoot, targetTokens) {
|
|
|
728
878
|
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
729
879
|
if (hasNeuron) {
|
|
730
880
|
const folderName = dir.split("/").pop() || "";
|
|
731
|
-
const
|
|
881
|
+
const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
882
|
+
const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
883
|
+
const existingTokens = tokenize(existingStripped);
|
|
732
884
|
const similarity = jaccardSimilarity(targetTokens, existingTokens);
|
|
733
|
-
if (
|
|
885
|
+
if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
|
|
886
|
+
} else if (similarity >= JACCARD_THRESHOLD) {
|
|
734
887
|
return dir;
|
|
735
888
|
}
|
|
736
889
|
}
|
|
737
890
|
for (const entry of entries) {
|
|
738
891
|
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
739
892
|
if (entry.isDirectory()) {
|
|
740
|
-
const match = findSimilar(
|
|
893
|
+
const match = findSimilar(join6(dir, entry.name), regionRoot, targetTokens, targetPrefix);
|
|
741
894
|
if (match) return match;
|
|
742
895
|
}
|
|
743
896
|
}
|
|
@@ -758,9 +911,9 @@ __export(rollback_exports, {
|
|
|
758
911
|
rollbackNeuron: () => rollbackNeuron
|
|
759
912
|
});
|
|
760
913
|
import { renameSync as renameSync2 } from "fs";
|
|
761
|
-
import { join as
|
|
914
|
+
import { join as join7 } from "path";
|
|
762
915
|
function rollbackNeuron(brainRoot, neuronPath) {
|
|
763
|
-
const fullPath =
|
|
916
|
+
const fullPath = join7(brainRoot, neuronPath);
|
|
764
917
|
const current = getCurrentCounter(fullPath);
|
|
765
918
|
if (current === 0) {
|
|
766
919
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
@@ -769,7 +922,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
|
|
|
769
922
|
throw new Error(`Counter already at minimum (1): ${neuronPath}`);
|
|
770
923
|
}
|
|
771
924
|
const newCounter = current - 1;
|
|
772
|
-
renameSync2(
|
|
925
|
+
renameSync2(join7(fullPath, `${current}.neuron`), join7(fullPath, `${newCounter}.neuron`));
|
|
773
926
|
console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
774
927
|
return newCounter;
|
|
775
928
|
}
|
|
@@ -785,31 +938,31 @@ var signal_exports = {};
|
|
|
785
938
|
__export(signal_exports, {
|
|
786
939
|
signalNeuron: () => signalNeuron
|
|
787
940
|
});
|
|
788
|
-
import { writeFileSync as
|
|
789
|
-
import { join as
|
|
941
|
+
import { writeFileSync as writeFileSync6, existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
|
|
942
|
+
import { join as join8 } from "path";
|
|
790
943
|
function signalNeuron(brainRoot, neuronPath, signalType) {
|
|
791
944
|
if (!SIGNAL_TYPES.includes(signalType)) {
|
|
792
945
|
throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
|
|
793
946
|
}
|
|
794
|
-
const fullPath =
|
|
795
|
-
if (!
|
|
947
|
+
const fullPath = join8(brainRoot, neuronPath);
|
|
948
|
+
if (!existsSync8(fullPath)) {
|
|
796
949
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
797
950
|
}
|
|
798
951
|
switch (signalType) {
|
|
799
952
|
case "bomb": {
|
|
800
|
-
|
|
953
|
+
writeFileSync6(join8(fullPath, "bomb.neuron"), "", "utf8");
|
|
801
954
|
console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
|
|
802
955
|
break;
|
|
803
956
|
}
|
|
804
957
|
case "dopamine": {
|
|
805
958
|
const level = getNextSignalLevel(fullPath, "dopamine");
|
|
806
|
-
|
|
959
|
+
writeFileSync6(join8(fullPath, `dopamine${level}.neuron`), "", "utf8");
|
|
807
960
|
console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
|
|
808
961
|
break;
|
|
809
962
|
}
|
|
810
963
|
case "memory": {
|
|
811
964
|
const level = getNextSignalLevel(fullPath, "memory");
|
|
812
|
-
|
|
965
|
+
writeFileSync6(join8(fullPath, `memory${level}.neuron`), "", "utf8");
|
|
813
966
|
console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
|
|
814
967
|
break;
|
|
815
968
|
}
|
|
@@ -840,15 +993,15 @@ var decay_exports = {};
|
|
|
840
993
|
__export(decay_exports, {
|
|
841
994
|
runDecay: () => runDecay
|
|
842
995
|
});
|
|
843
|
-
import { readdirSync as readdirSync6, statSync as
|
|
844
|
-
import { join as
|
|
996
|
+
import { readdirSync as readdirSync6, statSync as statSync3, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
|
|
997
|
+
import { join as join9 } from "path";
|
|
845
998
|
function runDecay(brainRoot, days) {
|
|
846
999
|
const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
847
1000
|
let scanned = 0;
|
|
848
1001
|
let decayed = 0;
|
|
849
1002
|
for (const regionName of REGIONS) {
|
|
850
|
-
const regionPath =
|
|
851
|
-
if (!
|
|
1003
|
+
const regionPath = join9(brainRoot, regionName);
|
|
1004
|
+
if (!existsSync9(regionPath)) continue;
|
|
852
1005
|
const result = decayWalk(regionPath, threshold, 0);
|
|
853
1006
|
scanned += result.scanned;
|
|
854
1007
|
decayed += result.decayed;
|
|
@@ -874,7 +1027,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
874
1027
|
if (entry.name.endsWith(".neuron")) {
|
|
875
1028
|
hasNeuronFile = true;
|
|
876
1029
|
try {
|
|
877
|
-
const st =
|
|
1030
|
+
const st = statSync3(join9(dir, entry.name));
|
|
878
1031
|
if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
|
|
879
1032
|
} catch {
|
|
880
1033
|
}
|
|
@@ -888,8 +1041,8 @@ function decayWalk(dir, threshold, depth) {
|
|
|
888
1041
|
scanned++;
|
|
889
1042
|
if (!isDormant && latestMod < threshold) {
|
|
890
1043
|
const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
|
|
891
|
-
|
|
892
|
-
|
|
1044
|
+
writeFileSync7(
|
|
1045
|
+
join9(dir, "decay.dormant"),
|
|
893
1046
|
`Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
|
|
894
1047
|
"utf8"
|
|
895
1048
|
);
|
|
@@ -899,7 +1052,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
899
1052
|
for (const entry of entries) {
|
|
900
1053
|
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
901
1054
|
if (entry.isDirectory()) {
|
|
902
|
-
const sub = decayWalk(
|
|
1055
|
+
const sub = decayWalk(join9(dir, entry.name), threshold, depth + 1);
|
|
903
1056
|
scanned += sub.scanned;
|
|
904
1057
|
decayed += sub.decayed;
|
|
905
1058
|
}
|
|
@@ -918,8 +1071,8 @@ var dedup_exports = {};
|
|
|
918
1071
|
__export(dedup_exports, {
|
|
919
1072
|
runDedup: () => runDedup
|
|
920
1073
|
});
|
|
921
|
-
import { writeFileSync as
|
|
922
|
-
import { join as
|
|
1074
|
+
import { writeFileSync as writeFileSync8 } from "fs";
|
|
1075
|
+
import { join as join10 } from "path";
|
|
923
1076
|
function runDedup(brainRoot) {
|
|
924
1077
|
const brain = scanBrain(brainRoot);
|
|
925
1078
|
let scanned = 0;
|
|
@@ -941,8 +1094,8 @@ function runDedup(brainRoot) {
|
|
|
941
1094
|
const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
|
|
942
1095
|
const relKeep = `${region.name}/${keep.path}`;
|
|
943
1096
|
fireNeuron(brainRoot, relKeep);
|
|
944
|
-
|
|
945
|
-
|
|
1097
|
+
writeFileSync8(
|
|
1098
|
+
join10(drop.fullPath, "dedup.dormant"),
|
|
946
1099
|
`Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
947
1100
|
"utf8"
|
|
948
1101
|
);
|
|
@@ -972,10 +1125,10 @@ __export(snapshot_exports, {
|
|
|
972
1125
|
gitSnapshot: () => gitSnapshot
|
|
973
1126
|
});
|
|
974
1127
|
import { execSync } from "child_process";
|
|
975
|
-
import { existsSync as
|
|
976
|
-
import { join as
|
|
1128
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1129
|
+
import { join as join11 } from "path";
|
|
977
1130
|
function gitSnapshot(brainRoot) {
|
|
978
|
-
if (!
|
|
1131
|
+
if (!existsSync10(join11(brainRoot, ".git"))) {
|
|
979
1132
|
try {
|
|
980
1133
|
execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
|
|
981
1134
|
} catch {
|
|
@@ -1064,12 +1217,12 @@ var init_watch = __esm({
|
|
|
1064
1217
|
});
|
|
1065
1218
|
|
|
1066
1219
|
// src/episode.ts
|
|
1067
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
1068
|
-
import { join as
|
|
1220
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
|
|
1221
|
+
import { join as join12 } from "path";
|
|
1069
1222
|
function logEpisode(brainRoot, type, path, detail) {
|
|
1070
|
-
const logDir =
|
|
1071
|
-
if (!
|
|
1072
|
-
|
|
1223
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1224
|
+
if (!existsSync11(logDir)) {
|
|
1225
|
+
mkdirSync6(logDir, { recursive: true });
|
|
1073
1226
|
}
|
|
1074
1227
|
const nextSlot = getNextSlot(logDir);
|
|
1075
1228
|
const episode = {
|
|
@@ -1078,12 +1231,35 @@ function logEpisode(brainRoot, type, path, detail) {
|
|
|
1078
1231
|
path,
|
|
1079
1232
|
detail
|
|
1080
1233
|
};
|
|
1081
|
-
|
|
1082
|
-
|
|
1234
|
+
writeFileSync9(
|
|
1235
|
+
join12(logDir, `memory${nextSlot}.neuron`),
|
|
1083
1236
|
JSON.stringify(episode),
|
|
1084
1237
|
"utf8"
|
|
1085
1238
|
);
|
|
1086
1239
|
}
|
|
1240
|
+
function readEpisodes(brainRoot) {
|
|
1241
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1242
|
+
if (!existsSync11(logDir)) return [];
|
|
1243
|
+
const episodes = [];
|
|
1244
|
+
let entries;
|
|
1245
|
+
try {
|
|
1246
|
+
entries = readdirSync7(logDir);
|
|
1247
|
+
} catch {
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
for (const entry of entries) {
|
|
1251
|
+
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1252
|
+
try {
|
|
1253
|
+
const content = readFileSync4(join12(logDir, entry), "utf8");
|
|
1254
|
+
if (content.trim()) {
|
|
1255
|
+
episodes.push(JSON.parse(content));
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1261
|
+
return episodes;
|
|
1262
|
+
}
|
|
1087
1263
|
function getNextSlot(logDir) {
|
|
1088
1264
|
let maxSlot = 0;
|
|
1089
1265
|
try {
|
|
@@ -1114,14 +1290,14 @@ __export(inbox_exports, {
|
|
|
1114
1290
|
ensureInbox: () => ensureInbox,
|
|
1115
1291
|
processInbox: () => processInbox
|
|
1116
1292
|
});
|
|
1117
|
-
import { readFileSync as
|
|
1118
|
-
import { join as
|
|
1293
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
1294
|
+
import { join as join13 } from "path";
|
|
1119
1295
|
function processInbox(brainRoot) {
|
|
1120
|
-
const inboxPath =
|
|
1121
|
-
if (!
|
|
1296
|
+
const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
|
|
1297
|
+
if (!existsSync12(inboxPath)) {
|
|
1122
1298
|
return { processed: 0, skipped: 0, errors: [] };
|
|
1123
1299
|
}
|
|
1124
|
-
const content =
|
|
1300
|
+
const content = readFileSync5(inboxPath, "utf8").trim();
|
|
1125
1301
|
if (!content) {
|
|
1126
1302
|
return { processed: 0, skipped: 0, errors: [] };
|
|
1127
1303
|
}
|
|
@@ -1162,7 +1338,7 @@ function processInbox(brainRoot) {
|
|
|
1162
1338
|
skipped++;
|
|
1163
1339
|
}
|
|
1164
1340
|
}
|
|
1165
|
-
|
|
1341
|
+
writeFileSync10(inboxPath, "", "utf8");
|
|
1166
1342
|
console.log(`\u{1F4E5} inbox: processed ${processed}, skipped ${skipped}`);
|
|
1167
1343
|
if (errors.length > 0) {
|
|
1168
1344
|
for (const err of errors) {
|
|
@@ -1173,9 +1349,9 @@ function processInbox(brainRoot) {
|
|
|
1173
1349
|
}
|
|
1174
1350
|
function applyCorrection(brainRoot, correction) {
|
|
1175
1351
|
const neuronPath = correction.path;
|
|
1176
|
-
const fullPath =
|
|
1352
|
+
const fullPath = join13(brainRoot, neuronPath);
|
|
1177
1353
|
const counterAdd = Math.max(1, correction.counter_add || 1);
|
|
1178
|
-
if (
|
|
1354
|
+
if (existsSync12(fullPath)) {
|
|
1179
1355
|
for (let i = 0; i < counterAdd; i++) {
|
|
1180
1356
|
fireNeuron(brainRoot, neuronPath);
|
|
1181
1357
|
}
|
|
@@ -1201,21 +1377,21 @@ function isPathSafe(path) {
|
|
|
1201
1377
|
return true;
|
|
1202
1378
|
}
|
|
1203
1379
|
function ensureInbox(brainRoot) {
|
|
1204
|
-
const inboxDir =
|
|
1205
|
-
if (!
|
|
1206
|
-
|
|
1380
|
+
const inboxDir = join13(brainRoot, INBOX_DIR);
|
|
1381
|
+
if (!existsSync12(inboxDir)) {
|
|
1382
|
+
mkdirSync7(inboxDir, { recursive: true });
|
|
1207
1383
|
}
|
|
1208
|
-
const filePath =
|
|
1209
|
-
if (!
|
|
1210
|
-
|
|
1384
|
+
const filePath = join13(inboxDir, CORRECTIONS_FILE);
|
|
1385
|
+
if (!existsSync12(filePath)) {
|
|
1386
|
+
writeFileSync10(filePath, "", "utf8");
|
|
1211
1387
|
}
|
|
1212
1388
|
return filePath;
|
|
1213
1389
|
}
|
|
1214
1390
|
function appendCorrection(brainRoot, correction) {
|
|
1215
1391
|
const filePath = ensureInbox(brainRoot);
|
|
1216
1392
|
const line = JSON.stringify(correction) + "\n";
|
|
1217
|
-
const existing =
|
|
1218
|
-
|
|
1393
|
+
const existing = readFileSync5(filePath, "utf8");
|
|
1394
|
+
writeFileSync10(filePath, existing + line, "utf8");
|
|
1219
1395
|
}
|
|
1220
1396
|
var INBOX_DIR, CORRECTIONS_FILE, DOPAMINE_ALLOWED_ROLES;
|
|
1221
1397
|
var init_inbox = __esm({
|
|
@@ -1519,22 +1695,28 @@ __export(hooks_exports, {
|
|
|
1519
1695
|
installHooks: () => installHooks,
|
|
1520
1696
|
uninstallHooks: () => uninstallHooks
|
|
1521
1697
|
});
|
|
1522
|
-
import { readFileSync as
|
|
1523
|
-
import {
|
|
1698
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
|
|
1699
|
+
import { execSync as execSync2 } from "child_process";
|
|
1700
|
+
import { join as join14, resolve as resolve2 } from "path";
|
|
1524
1701
|
function installHooks(brainRoot, projectRoot) {
|
|
1525
1702
|
const root = projectRoot || process.cwd();
|
|
1526
1703
|
const resolvedBrain = resolve2(brainRoot);
|
|
1527
|
-
if (!
|
|
1704
|
+
if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
|
|
1528
1705
|
initBrain(resolvedBrain);
|
|
1529
1706
|
}
|
|
1530
|
-
const settingsDir =
|
|
1531
|
-
const settingsPath =
|
|
1707
|
+
const settingsDir = join14(root, SETTINGS_DIR);
|
|
1708
|
+
const settingsPath = join14(settingsDir, SETTINGS_FILE);
|
|
1532
1709
|
const defaultBrain = resolve2(root, "brain");
|
|
1533
1710
|
const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
|
|
1711
|
+
let npxBin = "npx";
|
|
1712
|
+
try {
|
|
1713
|
+
npxBin = execSync2("which npx", { encoding: "utf8" }).trim();
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1534
1716
|
let settings = {};
|
|
1535
|
-
if (
|
|
1717
|
+
if (existsSync13(settingsPath)) {
|
|
1536
1718
|
try {
|
|
1537
|
-
settings = JSON.parse(
|
|
1719
|
+
settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
1538
1720
|
} catch {
|
|
1539
1721
|
console.log(`\u26A0\uFE0F settings.local.json was malformed, overwriting`);
|
|
1540
1722
|
}
|
|
@@ -1549,7 +1731,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1549
1731
|
matcher: "startup|resume",
|
|
1550
1732
|
entry: {
|
|
1551
1733
|
type: "command",
|
|
1552
|
-
command:
|
|
1734
|
+
command: `${npxBin} hebbian emit claude${brainFlag}`,
|
|
1553
1735
|
timeout: 10,
|
|
1554
1736
|
statusMessage: `${HOOK_MARKER} refreshing brain`
|
|
1555
1737
|
}
|
|
@@ -1558,7 +1740,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1558
1740
|
event: "Stop",
|
|
1559
1741
|
entry: {
|
|
1560
1742
|
type: "command",
|
|
1561
|
-
command:
|
|
1743
|
+
command: `${npxBin} hebbian digest${brainFlag}`,
|
|
1562
1744
|
timeout: 30,
|
|
1563
1745
|
statusMessage: `${HOOK_MARKER} digesting session`
|
|
1564
1746
|
}
|
|
@@ -1581,24 +1763,24 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1581
1763
|
hooks[event].push(group);
|
|
1582
1764
|
}
|
|
1583
1765
|
}
|
|
1584
|
-
if (!
|
|
1585
|
-
|
|
1766
|
+
if (!existsSync13(settingsDir)) {
|
|
1767
|
+
mkdirSync8(settingsDir, { recursive: true });
|
|
1586
1768
|
}
|
|
1587
|
-
|
|
1769
|
+
writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1588
1770
|
console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
|
|
1589
|
-
console.log(` SessionStart \u2192 hebbian emit claude${brainFlag}`);
|
|
1590
|
-
console.log(` Stop \u2192 hebbian digest${brainFlag}`);
|
|
1771
|
+
console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
|
|
1772
|
+
console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
|
|
1591
1773
|
}
|
|
1592
1774
|
function uninstallHooks(projectRoot) {
|
|
1593
1775
|
const root = projectRoot || process.cwd();
|
|
1594
|
-
const settingsPath =
|
|
1595
|
-
if (!
|
|
1776
|
+
const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
|
|
1777
|
+
if (!existsSync13(settingsPath)) {
|
|
1596
1778
|
console.log("No hooks installed (settings.local.json not found)");
|
|
1597
1779
|
return;
|
|
1598
1780
|
}
|
|
1599
1781
|
let settings;
|
|
1600
1782
|
try {
|
|
1601
|
-
settings = JSON.parse(
|
|
1783
|
+
settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
1602
1784
|
} catch {
|
|
1603
1785
|
console.log("settings.local.json is malformed, nothing to uninstall");
|
|
1604
1786
|
return;
|
|
@@ -1622,24 +1804,24 @@ function uninstallHooks(projectRoot) {
|
|
|
1622
1804
|
if (Object.keys(hooks).length === 0) {
|
|
1623
1805
|
delete settings.hooks;
|
|
1624
1806
|
}
|
|
1625
|
-
|
|
1807
|
+
writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1626
1808
|
console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
|
|
1627
1809
|
}
|
|
1628
1810
|
function checkHooks(projectRoot) {
|
|
1629
1811
|
const root = projectRoot || process.cwd();
|
|
1630
|
-
const settingsPath =
|
|
1812
|
+
const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
|
|
1631
1813
|
const status = {
|
|
1632
1814
|
installed: false,
|
|
1633
1815
|
path: settingsPath,
|
|
1634
1816
|
events: []
|
|
1635
1817
|
};
|
|
1636
|
-
if (!
|
|
1818
|
+
if (!existsSync13(settingsPath)) {
|
|
1637
1819
|
console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
|
|
1638
1820
|
return status;
|
|
1639
1821
|
}
|
|
1640
1822
|
let settings;
|
|
1641
1823
|
try {
|
|
1642
|
-
settings = JSON.parse(
|
|
1824
|
+
settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
1643
1825
|
} catch {
|
|
1644
1826
|
console.log(`\u274C settings.local.json is malformed`);
|
|
1645
1827
|
return status;
|
|
@@ -1669,7 +1851,7 @@ function checkHooks(projectRoot) {
|
|
|
1669
1851
|
return status;
|
|
1670
1852
|
}
|
|
1671
1853
|
function hasBrainRegions(dir) {
|
|
1672
|
-
if (!
|
|
1854
|
+
if (!existsSync13(dir)) return false;
|
|
1673
1855
|
try {
|
|
1674
1856
|
const entries = readdirSync8(dir);
|
|
1675
1857
|
return REGIONS.some((r) => entries.includes(r));
|
|
@@ -1695,8 +1877,8 @@ __export(digest_exports, {
|
|
|
1695
1877
|
extractCorrections: () => extractCorrections,
|
|
1696
1878
|
readHookInput: () => readHookInput
|
|
1697
1879
|
});
|
|
1698
|
-
import { readFileSync as
|
|
1699
|
-
import { join as
|
|
1880
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1881
|
+
import { join as join15, basename } from "path";
|
|
1700
1882
|
function readHookInput(stdin) {
|
|
1701
1883
|
if (!stdin.trim()) return null;
|
|
1702
1884
|
try {
|
|
@@ -1711,13 +1893,13 @@ function readHookInput(stdin) {
|
|
|
1711
1893
|
}
|
|
1712
1894
|
}
|
|
1713
1895
|
function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
1714
|
-
if (!
|
|
1896
|
+
if (!existsSync14(transcriptPath)) {
|
|
1715
1897
|
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
1716
1898
|
}
|
|
1717
1899
|
const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
|
|
1718
|
-
const logDir =
|
|
1719
|
-
const logPath =
|
|
1720
|
-
if (
|
|
1900
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
1901
|
+
const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
|
|
1902
|
+
if (existsSync14(logPath)) {
|
|
1721
1903
|
console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
|
|
1722
1904
|
return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
1723
1905
|
}
|
|
@@ -1751,7 +1933,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
1751
1933
|
};
|
|
1752
1934
|
}
|
|
1753
1935
|
function parseTranscript(transcriptPath) {
|
|
1754
|
-
const content =
|
|
1936
|
+
const content = readFileSync7(transcriptPath, "utf8");
|
|
1755
1937
|
const lines = content.split("\n").filter(Boolean);
|
|
1756
1938
|
const messages = [];
|
|
1757
1939
|
for (const line of lines) {
|
|
@@ -1793,17 +1975,24 @@ function extractCorrections(messages) {
|
|
|
1793
1975
|
}
|
|
1794
1976
|
function detectCorrection(text) {
|
|
1795
1977
|
const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
|
|
1978
|
+
const isMust = MUST_PATTERNS.some((p) => p.test(text));
|
|
1979
|
+
const isWarn = WARN_PATTERNS.some((p) => p.test(text));
|
|
1796
1980
|
const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
|
|
1797
|
-
if (!isNegation && !isAffirmation) return null;
|
|
1798
|
-
|
|
1981
|
+
if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
|
|
1982
|
+
let prefix;
|
|
1983
|
+
if (isNegation) prefix = "NO";
|
|
1984
|
+
else if (isMust) prefix = "MUST";
|
|
1985
|
+
else if (isWarn) prefix = "WARN";
|
|
1986
|
+
else prefix = "DO";
|
|
1799
1987
|
const keywords = extractKeywords(text);
|
|
1800
1988
|
if (keywords.length === 0) return null;
|
|
1801
|
-
const pathSegment = `${prefix}_${keywords.slice(0,
|
|
1989
|
+
const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
|
|
1802
1990
|
const path = `cortex/${pathSegment}`;
|
|
1803
1991
|
return { text, path, prefix, keywords };
|
|
1804
1992
|
}
|
|
1805
1993
|
function extractKeywords(text) {
|
|
1806
1994
|
const STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1995
|
+
// English stop words
|
|
1807
1996
|
"the",
|
|
1808
1997
|
"a",
|
|
1809
1998
|
"an",
|
|
@@ -1885,7 +2074,6 @@ function extractKeywords(text) {
|
|
|
1885
2074
|
"those",
|
|
1886
2075
|
"it",
|
|
1887
2076
|
"its",
|
|
1888
|
-
"i",
|
|
1889
2077
|
"me",
|
|
1890
2078
|
"my",
|
|
1891
2079
|
"we",
|
|
@@ -1914,17 +2102,40 @@ function extractKeywords(text) {
|
|
|
1914
2102
|
"should",
|
|
1915
2103
|
"like",
|
|
1916
2104
|
"want",
|
|
1917
|
-
"think"
|
|
2105
|
+
"think",
|
|
2106
|
+
"way",
|
|
2107
|
+
"make",
|
|
2108
|
+
"sure",
|
|
2109
|
+
"keep",
|
|
2110
|
+
"try",
|
|
2111
|
+
"let",
|
|
2112
|
+
"get",
|
|
2113
|
+
"put",
|
|
2114
|
+
"set",
|
|
2115
|
+
"new",
|
|
2116
|
+
"also",
|
|
2117
|
+
"using",
|
|
2118
|
+
"used",
|
|
2119
|
+
"when",
|
|
2120
|
+
"where",
|
|
2121
|
+
"how",
|
|
2122
|
+
"why",
|
|
2123
|
+
"here",
|
|
2124
|
+
"there",
|
|
2125
|
+
"careful",
|
|
2126
|
+
"warning",
|
|
2127
|
+
"watch",
|
|
2128
|
+
"out",
|
|
2129
|
+
"required"
|
|
1918
2130
|
]);
|
|
1919
|
-
|
|
1920
|
-
return tokens.filter((t) => !STOP_WORDS.has(t) && t.length > 2);
|
|
2131
|
+
return text.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(/\s+/).filter((t) => t.length > 2 && !STOP_WORDS.has(t));
|
|
1921
2132
|
}
|
|
1922
2133
|
function writeAuditLog(brainRoot, sessionId, entries) {
|
|
1923
|
-
const logDir =
|
|
1924
|
-
if (!
|
|
1925
|
-
|
|
2134
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
2135
|
+
if (!existsSync14(logDir)) {
|
|
2136
|
+
mkdirSync9(logDir, { recursive: true });
|
|
1926
2137
|
}
|
|
1927
|
-
const logPath =
|
|
2138
|
+
const logPath = join15(logDir, `${sessionId}.jsonl`);
|
|
1928
2139
|
const lines = entries.map(
|
|
1929
2140
|
(e) => JSON.stringify({
|
|
1930
2141
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1935,16 +2146,15 @@ function writeAuditLog(brainRoot, sessionId, entries) {
|
|
|
1935
2146
|
applied: e.applied
|
|
1936
2147
|
})
|
|
1937
2148
|
);
|
|
1938
|
-
|
|
2149
|
+
writeFileSync12(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
|
|
1939
2150
|
}
|
|
1940
|
-
var NEGATION_PATTERNS, AFFIRMATION_PATTERNS;
|
|
2151
|
+
var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS;
|
|
1941
2152
|
var init_digest = __esm({
|
|
1942
2153
|
"src/digest.ts"() {
|
|
1943
2154
|
"use strict";
|
|
1944
2155
|
init_constants();
|
|
1945
2156
|
init_grow();
|
|
1946
2157
|
init_episode();
|
|
1947
|
-
init_similarity();
|
|
1948
2158
|
NEGATION_PATTERNS = [
|
|
1949
2159
|
/\bdon[''\u2019]?t\b/i,
|
|
1950
2160
|
/\bdo not\b/i,
|
|
@@ -1963,13 +2173,281 @@ var init_digest = __esm({
|
|
|
1963
2173
|
];
|
|
1964
2174
|
AFFIRMATION_PATTERNS = [
|
|
1965
2175
|
/\balways\b/i,
|
|
1966
|
-
/\bmust\b/i,
|
|
1967
2176
|
/\bshould\s+always\b/i,
|
|
1968
2177
|
/\buse\s+\w+\s+instead\b/i,
|
|
1969
2178
|
// Korean affirmation
|
|
1970
|
-
|
|
2179
|
+
/항상/
|
|
2180
|
+
];
|
|
2181
|
+
MUST_PATTERNS = [
|
|
2182
|
+
/\bmust\b/i,
|
|
2183
|
+
/\brequired\b/i,
|
|
2184
|
+
// Korean
|
|
1971
2185
|
/반드시/
|
|
1972
2186
|
];
|
|
2187
|
+
WARN_PATTERNS = [
|
|
2188
|
+
/\bcareful\b/i,
|
|
2189
|
+
/\bwatch\s+out\b/i,
|
|
2190
|
+
/\bwarning\b/i,
|
|
2191
|
+
// Korean
|
|
2192
|
+
/주의/
|
|
2193
|
+
];
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
// src/evolve.ts
|
|
2198
|
+
var evolve_exports = {};
|
|
2199
|
+
__export(evolve_exports, {
|
|
2200
|
+
buildBrainSummary: () => buildBrainSummary,
|
|
2201
|
+
buildPrompt: () => buildPrompt,
|
|
2202
|
+
callGemini: () => callGemini,
|
|
2203
|
+
executeActions: () => executeActions,
|
|
2204
|
+
parseActions: () => parseActions,
|
|
2205
|
+
runEvolve: () => runEvolve,
|
|
2206
|
+
validateActions: () => validateActions
|
|
2207
|
+
});
|
|
2208
|
+
async function runEvolve(brainRoot, dryRun) {
|
|
2209
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
2210
|
+
if (!apiKey) {
|
|
2211
|
+
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
2212
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2213
|
+
}
|
|
2214
|
+
const episodes = readEpisodes(brainRoot);
|
|
2215
|
+
const brain = scanBrain(brainRoot);
|
|
2216
|
+
const summary = buildBrainSummary(brain);
|
|
2217
|
+
const prompt = buildPrompt(summary, episodes);
|
|
2218
|
+
let rawActions;
|
|
2219
|
+
try {
|
|
2220
|
+
rawActions = await callGemini(prompt, apiKey);
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
const msg = err.message;
|
|
2223
|
+
console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
|
|
2224
|
+
logEpisode(brainRoot, "evolve-error", "", msg);
|
|
2225
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2226
|
+
}
|
|
2227
|
+
const actions = validateActions(rawActions, brain);
|
|
2228
|
+
const skipped = rawActions.length - actions.length;
|
|
2229
|
+
if (actions.length === 0) {
|
|
2230
|
+
console.log("\u{1F9E0} evolve: no valid actions proposed");
|
|
2231
|
+
return { actions: [], executed: 0, skipped, dryRun };
|
|
2232
|
+
}
|
|
2233
|
+
if (dryRun) {
|
|
2234
|
+
console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
|
|
2235
|
+
for (const action of actions) {
|
|
2236
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
|
|
2237
|
+
}
|
|
2238
|
+
return { actions, executed: 0, skipped, dryRun: true };
|
|
2239
|
+
}
|
|
2240
|
+
const executed = executeActions(brainRoot, actions);
|
|
2241
|
+
logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
|
|
2242
|
+
console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
|
|
2243
|
+
return { actions, executed, skipped, dryRun: false };
|
|
2244
|
+
}
|
|
2245
|
+
function buildBrainSummary(brain) {
|
|
2246
|
+
const lines = ["# Brain State\n"];
|
|
2247
|
+
for (const region of brain.regions) {
|
|
2248
|
+
const neurons = region.neurons;
|
|
2249
|
+
if (neurons.length === 0 && !region.hasBomb) continue;
|
|
2250
|
+
lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
|
|
2251
|
+
if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
|
|
2252
|
+
for (const neuron of neurons) {
|
|
2253
|
+
const flags = [];
|
|
2254
|
+
if (neuron.isDormant) flags.push("dormant");
|
|
2255
|
+
if (neuron.hasBomb) flags.push("bomb");
|
|
2256
|
+
if (neuron.hasMemory) flags.push("memory");
|
|
2257
|
+
if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
|
|
2258
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
2259
|
+
lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
|
|
2260
|
+
}
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
}
|
|
2263
|
+
return lines.join("\n");
|
|
2264
|
+
}
|
|
2265
|
+
function buildPrompt(summary, episodes) {
|
|
2266
|
+
const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
|
|
2267
|
+
return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
2268
|
+
|
|
2269
|
+
## Axioms
|
|
2270
|
+
- Folder = Neuron, File = Firing Trace, Counter = Activation strength
|
|
2271
|
+
- 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
|
|
2272
|
+
- Lower priority ALWAYS overrides higher priority
|
|
2273
|
+
- PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
|
|
2274
|
+
|
|
2275
|
+
## Current Brain
|
|
2276
|
+
${summary}
|
|
2277
|
+
|
|
2278
|
+
## Recent Episodes (last ${episodes.length})
|
|
2279
|
+
${episodeLines}
|
|
2280
|
+
|
|
2281
|
+
## Available Actions
|
|
2282
|
+
- grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
|
|
2283
|
+
- fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
|
|
2284
|
+
- signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
|
|
2285
|
+
- prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
|
|
2286
|
+
- decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
|
|
2287
|
+
|
|
2288
|
+
## Constraints
|
|
2289
|
+
- Max ${MAX_ACTIONS} actions per cycle
|
|
2290
|
+
- PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
|
|
2291
|
+
- NEVER target brainstem, limbic, or sensors regions
|
|
2292
|
+
- Each action needs a "reason" explaining why
|
|
2293
|
+
|
|
2294
|
+
## Task
|
|
2295
|
+
Analyze the brain state and recent episodes. Propose actions to improve the brain.
|
|
2296
|
+
Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
|
|
2297
|
+
|
|
2298
|
+
Respond with a JSON array of actions:
|
|
2299
|
+
[{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
|
|
2300
|
+
}
|
|
2301
|
+
async function callGemini(prompt, apiKey) {
|
|
2302
|
+
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
2303
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
2304
|
+
const body = {
|
|
2305
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
2306
|
+
generationConfig: {
|
|
2307
|
+
responseMimeType: "application/json",
|
|
2308
|
+
temperature: 0.2
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
let lastError = null;
|
|
2312
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2313
|
+
if (attempt > 0) {
|
|
2314
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY));
|
|
2315
|
+
}
|
|
2316
|
+
try {
|
|
2317
|
+
const res = await fetch(url, {
|
|
2318
|
+
method: "POST",
|
|
2319
|
+
headers: { "Content-Type": "application/json" },
|
|
2320
|
+
body: JSON.stringify(body),
|
|
2321
|
+
signal: AbortSignal.timeout(API_TIMEOUT)
|
|
2322
|
+
});
|
|
2323
|
+
if (!res.ok) {
|
|
2324
|
+
lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
const data = await res.json();
|
|
2328
|
+
if (data.error) {
|
|
2329
|
+
lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
2333
|
+
if (!text) {
|
|
2334
|
+
lastError = new Error("Gemini returned empty response");
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
return parseActions(text);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
lastError = err;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
throw lastError || new Error("Gemini call failed");
|
|
2344
|
+
}
|
|
2345
|
+
function parseActions(text) {
|
|
2346
|
+
let parsed;
|
|
2347
|
+
try {
|
|
2348
|
+
parsed = JSON.parse(text);
|
|
2349
|
+
} catch {
|
|
2350
|
+
throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
|
|
2351
|
+
}
|
|
2352
|
+
if (!Array.isArray(parsed)) {
|
|
2353
|
+
throw new Error("LLM response is not an array");
|
|
2354
|
+
}
|
|
2355
|
+
const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
|
|
2356
|
+
const actions = [];
|
|
2357
|
+
for (const item of parsed) {
|
|
2358
|
+
if (!item || typeof item !== "object") continue;
|
|
2359
|
+
const { type, path, reason, signal } = item;
|
|
2360
|
+
if (typeof type !== "string" || !validTypes.has(type)) continue;
|
|
2361
|
+
if (typeof path !== "string" || path.length === 0) continue;
|
|
2362
|
+
if (typeof reason !== "string") continue;
|
|
2363
|
+
const action = { type, path, reason };
|
|
2364
|
+
if (type === "signal" && typeof signal === "string") {
|
|
2365
|
+
action.signal = signal;
|
|
2366
|
+
}
|
|
2367
|
+
actions.push(action);
|
|
2368
|
+
}
|
|
2369
|
+
return actions;
|
|
2370
|
+
}
|
|
2371
|
+
function validateActions(actions, _brain) {
|
|
2372
|
+
return actions.filter((action) => {
|
|
2373
|
+
const region = action.path.split("/")[0];
|
|
2374
|
+
if (!region || PROTECTED_REGIONS.includes(region)) {
|
|
2375
|
+
console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
|
|
2376
|
+
return false;
|
|
2377
|
+
}
|
|
2378
|
+
if (!REGIONS.includes(region)) {
|
|
2379
|
+
console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
|
|
2383
|
+
console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2386
|
+
return true;
|
|
2387
|
+
}).slice(0, MAX_ACTIONS);
|
|
2388
|
+
}
|
|
2389
|
+
function executeActions(brainRoot, actions) {
|
|
2390
|
+
let executed = 0;
|
|
2391
|
+
for (const action of actions) {
|
|
2392
|
+
try {
|
|
2393
|
+
switch (action.type) {
|
|
2394
|
+
case "fire":
|
|
2395
|
+
fireNeuron(brainRoot, action.path);
|
|
2396
|
+
break;
|
|
2397
|
+
case "grow":
|
|
2398
|
+
growNeuron(brainRoot, action.path);
|
|
2399
|
+
break;
|
|
2400
|
+
case "signal":
|
|
2401
|
+
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
2402
|
+
break;
|
|
2403
|
+
case "prune":
|
|
2404
|
+
rollbackNeuron(brainRoot, action.path);
|
|
2405
|
+
break;
|
|
2406
|
+
case "decay":
|
|
2407
|
+
runDecay(brainRoot, 0);
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
|
|
2411
|
+
executed++;
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return executed;
|
|
2417
|
+
}
|
|
2418
|
+
function actionIcon(type) {
|
|
2419
|
+
switch (type) {
|
|
2420
|
+
case "fire":
|
|
2421
|
+
return "\u{1F525}";
|
|
2422
|
+
case "grow":
|
|
2423
|
+
return "\u{1F331}";
|
|
2424
|
+
case "signal":
|
|
2425
|
+
return "\u26A1";
|
|
2426
|
+
case "prune":
|
|
2427
|
+
return "\u2702\uFE0F";
|
|
2428
|
+
case "decay":
|
|
2429
|
+
return "\u{1F4A4}";
|
|
2430
|
+
default:
|
|
2431
|
+
return "\u2753";
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
|
|
2435
|
+
var init_evolve = __esm({
|
|
2436
|
+
"src/evolve.ts"() {
|
|
2437
|
+
"use strict";
|
|
2438
|
+
init_episode();
|
|
2439
|
+
init_scanner();
|
|
2440
|
+
init_constants();
|
|
2441
|
+
init_fire();
|
|
2442
|
+
init_grow();
|
|
2443
|
+
init_signal();
|
|
2444
|
+
init_rollback();
|
|
2445
|
+
init_decay();
|
|
2446
|
+
MAX_ACTIONS = 10;
|
|
2447
|
+
PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
|
|
2448
|
+
DEFAULT_MODEL = "gemini-2.0-flash-lite";
|
|
2449
|
+
API_TIMEOUT = 3e4;
|
|
2450
|
+
RETRY_DELAY = 5e3;
|
|
1973
2451
|
}
|
|
1974
2452
|
});
|
|
1975
2453
|
|
|
@@ -2001,6 +2479,7 @@ COMMANDS:
|
|
|
2001
2479
|
inbox Process corrections inbox
|
|
2002
2480
|
claude install|uninstall|status Manage Claude Code hooks
|
|
2003
2481
|
digest [--transcript <path>] Extract corrections from conversation
|
|
2482
|
+
evolve [--dry-run] LLM-powered brain evolution (Gemini)
|
|
2004
2483
|
diag Print brain diagnostics
|
|
2005
2484
|
stats Print brain statistics
|
|
2006
2485
|
|
|
@@ -2015,6 +2494,7 @@ EXAMPLES:
|
|
|
2015
2494
|
hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
|
|
2016
2495
|
hebbian emit claude --brain ./my-brain
|
|
2017
2496
|
hebbian emit all
|
|
2497
|
+
GEMINI_API_KEY=... hebbian evolve --dry-run
|
|
2018
2498
|
`.trim();
|
|
2019
2499
|
function readStdin() {
|
|
2020
2500
|
return new Promise((resolve4) => {
|
|
@@ -2040,6 +2520,7 @@ async function main(argv) {
|
|
|
2040
2520
|
days: { type: "string", short: "d" },
|
|
2041
2521
|
port: { type: "string", short: "p" },
|
|
2042
2522
|
transcript: { type: "string", short: "t" },
|
|
2523
|
+
"dry-run": { type: "boolean" },
|
|
2043
2524
|
help: { type: "boolean", short: "h" },
|
|
2044
2525
|
version: { type: "boolean", short: "v" }
|
|
2045
2526
|
},
|
|
@@ -2075,6 +2556,12 @@ async function main(argv) {
|
|
|
2075
2556
|
}
|
|
2076
2557
|
const { emitToTarget: emitToTarget2 } = await Promise.resolve().then(() => (init_emit(), emit_exports));
|
|
2077
2558
|
await emitToTarget2(brainRoot, target);
|
|
2559
|
+
const { checkForUpdates: checkForUpdates2, formatUpdateBanner: formatUpdateBanner2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
2560
|
+
checkForUpdates2(VERSION).then((status) => {
|
|
2561
|
+
const banner = formatUpdateBanner2(status);
|
|
2562
|
+
if (banner) console.error(banner);
|
|
2563
|
+
}).catch(() => {
|
|
2564
|
+
});
|
|
2078
2565
|
break;
|
|
2079
2566
|
}
|
|
2080
2567
|
case "fire": {
|
|
@@ -2164,9 +2651,15 @@ async function main(argv) {
|
|
|
2164
2651
|
case "uninstall":
|
|
2165
2652
|
uninstallHooks2();
|
|
2166
2653
|
break;
|
|
2167
|
-
case "status":
|
|
2654
|
+
case "status": {
|
|
2168
2655
|
checkHooks2();
|
|
2656
|
+
console.log(` version: v${VERSION}`);
|
|
2657
|
+
const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
2658
|
+
const updateStatus = await checkUpdates(VERSION);
|
|
2659
|
+
const updateBanner = formatBanner(updateStatus);
|
|
2660
|
+
if (updateBanner) console.error(updateBanner);
|
|
2169
2661
|
break;
|
|
2662
|
+
}
|
|
2170
2663
|
default:
|
|
2171
2664
|
console.error("Usage: hebbian claude <install|uninstall|status>");
|
|
2172
2665
|
process.exit(1);
|
|
@@ -2191,6 +2684,12 @@ async function main(argv) {
|
|
|
2191
2684
|
}
|
|
2192
2685
|
break;
|
|
2193
2686
|
}
|
|
2687
|
+
case "evolve": {
|
|
2688
|
+
const dryRun = values["dry-run"] === true;
|
|
2689
|
+
const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
|
|
2690
|
+
await runEvolve2(brainRoot, dryRun);
|
|
2691
|
+
break;
|
|
2692
|
+
}
|
|
2194
2693
|
case "diag":
|
|
2195
2694
|
case "stats": {
|
|
2196
2695
|
const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|