hebbian 0.3.2 → 0.3.3

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.
@@ -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 writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
617
- import { join as join4 } from "path";
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 = join4(brainRoot, neuronPath);
620
- if (!existsSync5(fullPath)) {
621
- mkdirSync3(fullPath, { recursive: true });
622
- writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
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(join4(fullPath, `${current}.neuron`), join4(fullPath, `${newCounter}.neuron`));
777
+ renameSync(join5(fullPath, `${current}.neuron`), join5(fullPath, `${newCounter}.neuron`));
630
778
  } else {
631
- writeFileSync3(join4(fullPath, `${newCounter}.neuron`), "", "utf8");
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(/[_\-\s]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
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 mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, readdirSync as readdirSync4 } from "fs";
692
- import { join as join5, relative as relative2 } from "path";
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 = join5(brainRoot, neuronPath);
695
- if (existsSync6(fullPath)) {
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 newTokens = tokenize(leafName);
706
- const regionPath = join5(brainRoot, regionName);
707
- if (existsSync6(regionPath)) {
708
- const match = findSimilar(regionPath, regionPath, newTokens);
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
- mkdirSync4(fullPath, { recursive: true });
717
- writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
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 existingTokens = tokenize(folderName);
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 (similarity >= JACCARD_THRESHOLD) {
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(join5(dir, entry.name), regionRoot, targetTokens);
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 join6 } from "path";
914
+ import { join as join7 } from "path";
762
915
  function rollbackNeuron(brainRoot, neuronPath) {
763
- const fullPath = join6(brainRoot, neuronPath);
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(join6(fullPath, `${current}.neuron`), join6(fullPath, `${newCounter}.neuron`));
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 writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
789
- import { join as join7 } from "path";
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 = join7(brainRoot, neuronPath);
795
- if (!existsSync7(fullPath)) {
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
- writeFileSync5(join7(fullPath, "bomb.neuron"), "", "utf8");
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
- writeFileSync5(join7(fullPath, `dopamine${level}.neuron`), "", "utf8");
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
- writeFileSync5(join7(fullPath, `memory${level}.neuron`), "", "utf8");
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 statSync2, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
844
- import { join as join8 } from "path";
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 = join8(brainRoot, regionName);
851
- if (!existsSync8(regionPath)) continue;
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 = statSync2(join8(dir, entry.name));
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
- writeFileSync6(
892
- join8(dir, "decay.dormant"),
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(join8(dir, entry.name), threshold, depth + 1);
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 writeFileSync7 } from "fs";
922
- import { join as join9 } from "path";
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
- writeFileSync7(
945
- join9(drop.fullPath, "dedup.dormant"),
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 existsSync9 } from "fs";
976
- import { join as join10 } from "path";
1128
+ import { existsSync as existsSync10 } from "fs";
1129
+ import { join as join11 } from "path";
977
1130
  function gitSnapshot(brainRoot) {
978
- if (!existsSync9(join10(brainRoot, ".git"))) {
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 readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
1068
- import { join as join11 } from "path";
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 = join11(brainRoot, SESSION_LOG_DIR);
1071
- if (!existsSync10(logDir)) {
1072
- mkdirSync5(logDir, { recursive: true });
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,8 +1231,8 @@ function logEpisode(brainRoot, type, path, detail) {
1078
1231
  path,
1079
1232
  detail
1080
1233
  };
1081
- writeFileSync8(
1082
- join11(logDir, `memory${nextSlot}.neuron`),
1234
+ writeFileSync9(
1235
+ join12(logDir, `memory${nextSlot}.neuron`),
1083
1236
  JSON.stringify(episode),
1084
1237
  "utf8"
1085
1238
  );
@@ -1114,14 +1267,14 @@ __export(inbox_exports, {
1114
1267
  ensureInbox: () => ensureInbox,
1115
1268
  processInbox: () => processInbox
1116
1269
  });
1117
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
1118
- import { join as join12 } from "path";
1270
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1271
+ import { join as join13 } from "path";
1119
1272
  function processInbox(brainRoot) {
1120
- const inboxPath = join12(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1121
- if (!existsSync11(inboxPath)) {
1273
+ const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1274
+ if (!existsSync12(inboxPath)) {
1122
1275
  return { processed: 0, skipped: 0, errors: [] };
1123
1276
  }
1124
- const content = readFileSync4(inboxPath, "utf8").trim();
1277
+ const content = readFileSync5(inboxPath, "utf8").trim();
1125
1278
  if (!content) {
1126
1279
  return { processed: 0, skipped: 0, errors: [] };
1127
1280
  }
@@ -1162,7 +1315,7 @@ function processInbox(brainRoot) {
1162
1315
  skipped++;
1163
1316
  }
1164
1317
  }
1165
- writeFileSync9(inboxPath, "", "utf8");
1318
+ writeFileSync10(inboxPath, "", "utf8");
1166
1319
  console.log(`\u{1F4E5} inbox: processed ${processed}, skipped ${skipped}`);
1167
1320
  if (errors.length > 0) {
1168
1321
  for (const err of errors) {
@@ -1173,9 +1326,9 @@ function processInbox(brainRoot) {
1173
1326
  }
1174
1327
  function applyCorrection(brainRoot, correction) {
1175
1328
  const neuronPath = correction.path;
1176
- const fullPath = join12(brainRoot, neuronPath);
1329
+ const fullPath = join13(brainRoot, neuronPath);
1177
1330
  const counterAdd = Math.max(1, correction.counter_add || 1);
1178
- if (existsSync11(fullPath)) {
1331
+ if (existsSync12(fullPath)) {
1179
1332
  for (let i = 0; i < counterAdd; i++) {
1180
1333
  fireNeuron(brainRoot, neuronPath);
1181
1334
  }
@@ -1201,21 +1354,21 @@ function isPathSafe(path) {
1201
1354
  return true;
1202
1355
  }
1203
1356
  function ensureInbox(brainRoot) {
1204
- const inboxDir = join12(brainRoot, INBOX_DIR);
1205
- if (!existsSync11(inboxDir)) {
1206
- mkdirSync6(inboxDir, { recursive: true });
1357
+ const inboxDir = join13(brainRoot, INBOX_DIR);
1358
+ if (!existsSync12(inboxDir)) {
1359
+ mkdirSync7(inboxDir, { recursive: true });
1207
1360
  }
1208
- const filePath = join12(inboxDir, CORRECTIONS_FILE);
1209
- if (!existsSync11(filePath)) {
1210
- writeFileSync9(filePath, "", "utf8");
1361
+ const filePath = join13(inboxDir, CORRECTIONS_FILE);
1362
+ if (!existsSync12(filePath)) {
1363
+ writeFileSync10(filePath, "", "utf8");
1211
1364
  }
1212
1365
  return filePath;
1213
1366
  }
1214
1367
  function appendCorrection(brainRoot, correction) {
1215
1368
  const filePath = ensureInbox(brainRoot);
1216
1369
  const line = JSON.stringify(correction) + "\n";
1217
- const existing = readFileSync4(filePath, "utf8");
1218
- writeFileSync9(filePath, existing + line, "utf8");
1370
+ const existing = readFileSync5(filePath, "utf8");
1371
+ writeFileSync10(filePath, existing + line, "utf8");
1219
1372
  }
1220
1373
  var INBOX_DIR, CORRECTIONS_FILE, DOPAMINE_ALLOWED_ROLES;
1221
1374
  var init_inbox = __esm({
@@ -1519,22 +1672,28 @@ __export(hooks_exports, {
1519
1672
  installHooks: () => installHooks,
1520
1673
  uninstallHooks: () => uninstallHooks
1521
1674
  });
1522
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8 } from "fs";
1523
- import { join as join13, resolve as resolve2 } from "path";
1675
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
1676
+ import { execSync as execSync2 } from "child_process";
1677
+ import { join as join14, resolve as resolve2 } from "path";
1524
1678
  function installHooks(brainRoot, projectRoot) {
1525
1679
  const root = projectRoot || process.cwd();
1526
1680
  const resolvedBrain = resolve2(brainRoot);
1527
- if (!existsSync12(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1681
+ if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1528
1682
  initBrain(resolvedBrain);
1529
1683
  }
1530
- const settingsDir = join13(root, SETTINGS_DIR);
1531
- const settingsPath = join13(settingsDir, SETTINGS_FILE);
1684
+ const settingsDir = join14(root, SETTINGS_DIR);
1685
+ const settingsPath = join14(settingsDir, SETTINGS_FILE);
1532
1686
  const defaultBrain = resolve2(root, "brain");
1533
1687
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1688
+ let npxBin = "npx";
1689
+ try {
1690
+ npxBin = execSync2("which npx", { encoding: "utf8" }).trim();
1691
+ } catch {
1692
+ }
1534
1693
  let settings = {};
1535
- if (existsSync12(settingsPath)) {
1694
+ if (existsSync13(settingsPath)) {
1536
1695
  try {
1537
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1696
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1538
1697
  } catch {
1539
1698
  console.log(`\u26A0\uFE0F settings.local.json was malformed, overwriting`);
1540
1699
  }
@@ -1549,7 +1708,7 @@ function installHooks(brainRoot, projectRoot) {
1549
1708
  matcher: "startup|resume",
1550
1709
  entry: {
1551
1710
  type: "command",
1552
- command: `hebbian emit claude${brainFlag}`,
1711
+ command: `${npxBin} hebbian emit claude${brainFlag}`,
1553
1712
  timeout: 10,
1554
1713
  statusMessage: `${HOOK_MARKER} refreshing brain`
1555
1714
  }
@@ -1558,7 +1717,7 @@ function installHooks(brainRoot, projectRoot) {
1558
1717
  event: "Stop",
1559
1718
  entry: {
1560
1719
  type: "command",
1561
- command: `hebbian digest${brainFlag}`,
1720
+ command: `${npxBin} hebbian digest${brainFlag}`,
1562
1721
  timeout: 30,
1563
1722
  statusMessage: `${HOOK_MARKER} digesting session`
1564
1723
  }
@@ -1581,24 +1740,24 @@ function installHooks(brainRoot, projectRoot) {
1581
1740
  hooks[event].push(group);
1582
1741
  }
1583
1742
  }
1584
- if (!existsSync12(settingsDir)) {
1585
- mkdirSync7(settingsDir, { recursive: true });
1743
+ if (!existsSync13(settingsDir)) {
1744
+ mkdirSync8(settingsDir, { recursive: true });
1586
1745
  }
1587
- writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1746
+ writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1588
1747
  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}`);
1748
+ console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1749
+ console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1591
1750
  }
1592
1751
  function uninstallHooks(projectRoot) {
1593
1752
  const root = projectRoot || process.cwd();
1594
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1595
- if (!existsSync12(settingsPath)) {
1753
+ const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1754
+ if (!existsSync13(settingsPath)) {
1596
1755
  console.log("No hooks installed (settings.local.json not found)");
1597
1756
  return;
1598
1757
  }
1599
1758
  let settings;
1600
1759
  try {
1601
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1760
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1602
1761
  } catch {
1603
1762
  console.log("settings.local.json is malformed, nothing to uninstall");
1604
1763
  return;
@@ -1622,24 +1781,24 @@ function uninstallHooks(projectRoot) {
1622
1781
  if (Object.keys(hooks).length === 0) {
1623
1782
  delete settings.hooks;
1624
1783
  }
1625
- writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1784
+ writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1626
1785
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1627
1786
  }
1628
1787
  function checkHooks(projectRoot) {
1629
1788
  const root = projectRoot || process.cwd();
1630
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1789
+ const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1631
1790
  const status = {
1632
1791
  installed: false,
1633
1792
  path: settingsPath,
1634
1793
  events: []
1635
1794
  };
1636
- if (!existsSync12(settingsPath)) {
1795
+ if (!existsSync13(settingsPath)) {
1637
1796
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1638
1797
  return status;
1639
1798
  }
1640
1799
  let settings;
1641
1800
  try {
1642
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1801
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1643
1802
  } catch {
1644
1803
  console.log(`\u274C settings.local.json is malformed`);
1645
1804
  return status;
@@ -1669,7 +1828,7 @@ function checkHooks(projectRoot) {
1669
1828
  return status;
1670
1829
  }
1671
1830
  function hasBrainRegions(dir) {
1672
- if (!existsSync12(dir)) return false;
1831
+ if (!existsSync13(dir)) return false;
1673
1832
  try {
1674
1833
  const entries = readdirSync8(dir);
1675
1834
  return REGIONS.some((r) => entries.includes(r));
@@ -1695,8 +1854,8 @@ __export(digest_exports, {
1695
1854
  extractCorrections: () => extractCorrections,
1696
1855
  readHookInput: () => readHookInput
1697
1856
  });
1698
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1699
- import { join as join14, basename } from "path";
1857
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1858
+ import { join as join15, basename } from "path";
1700
1859
  function readHookInput(stdin) {
1701
1860
  if (!stdin.trim()) return null;
1702
1861
  try {
@@ -1711,13 +1870,13 @@ function readHookInput(stdin) {
1711
1870
  }
1712
1871
  }
1713
1872
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1714
- if (!existsSync13(transcriptPath)) {
1873
+ if (!existsSync14(transcriptPath)) {
1715
1874
  throw new Error(`Transcript not found: ${transcriptPath}`);
1716
1875
  }
1717
1876
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1718
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1719
- const logPath = join14(logDir, `${resolvedSessionId}.jsonl`);
1720
- if (existsSync13(logPath)) {
1877
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1878
+ const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1879
+ if (existsSync14(logPath)) {
1721
1880
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1722
1881
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1723
1882
  }
@@ -1751,7 +1910,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1751
1910
  };
1752
1911
  }
1753
1912
  function parseTranscript(transcriptPath) {
1754
- const content = readFileSync6(transcriptPath, "utf8");
1913
+ const content = readFileSync7(transcriptPath, "utf8");
1755
1914
  const lines = content.split("\n").filter(Boolean);
1756
1915
  const messages = [];
1757
1916
  for (const line of lines) {
@@ -1793,17 +1952,24 @@ function extractCorrections(messages) {
1793
1952
  }
1794
1953
  function detectCorrection(text) {
1795
1954
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
1955
+ const isMust = MUST_PATTERNS.some((p) => p.test(text));
1956
+ const isWarn = WARN_PATTERNS.some((p) => p.test(text));
1796
1957
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
1797
- if (!isNegation && !isAffirmation) return null;
1798
- const prefix = isNegation ? "NO" : "DO";
1958
+ if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
1959
+ let prefix;
1960
+ if (isNegation) prefix = "NO";
1961
+ else if (isMust) prefix = "MUST";
1962
+ else if (isWarn) prefix = "WARN";
1963
+ else prefix = "DO";
1799
1964
  const keywords = extractKeywords(text);
1800
1965
  if (keywords.length === 0) return null;
1801
- const pathSegment = `${prefix}_${keywords.slice(0, 4).join("_")}`;
1966
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
1802
1967
  const path = `cortex/${pathSegment}`;
1803
1968
  return { text, path, prefix, keywords };
1804
1969
  }
1805
1970
  function extractKeywords(text) {
1806
1971
  const STOP_WORDS = /* @__PURE__ */ new Set([
1972
+ // English stop words
1807
1973
  "the",
1808
1974
  "a",
1809
1975
  "an",
@@ -1885,7 +2051,6 @@ function extractKeywords(text) {
1885
2051
  "those",
1886
2052
  "it",
1887
2053
  "its",
1888
- "i",
1889
2054
  "me",
1890
2055
  "my",
1891
2056
  "we",
@@ -1914,17 +2079,40 @@ function extractKeywords(text) {
1914
2079
  "should",
1915
2080
  "like",
1916
2081
  "want",
1917
- "think"
2082
+ "think",
2083
+ "way",
2084
+ "make",
2085
+ "sure",
2086
+ "keep",
2087
+ "try",
2088
+ "let",
2089
+ "get",
2090
+ "put",
2091
+ "set",
2092
+ "new",
2093
+ "also",
2094
+ "using",
2095
+ "used",
2096
+ "when",
2097
+ "where",
2098
+ "how",
2099
+ "why",
2100
+ "here",
2101
+ "there",
2102
+ "careful",
2103
+ "warning",
2104
+ "watch",
2105
+ "out",
2106
+ "required"
1918
2107
  ]);
1919
- const tokens = tokenize(text);
1920
- return tokens.filter((t) => !STOP_WORDS.has(t) && t.length > 2);
2108
+ 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
2109
  }
1922
2110
  function writeAuditLog(brainRoot, sessionId, entries) {
1923
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1924
- if (!existsSync13(logDir)) {
1925
- mkdirSync8(logDir, { recursive: true });
2111
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2112
+ if (!existsSync14(logDir)) {
2113
+ mkdirSync9(logDir, { recursive: true });
1926
2114
  }
1927
- const logPath = join14(logDir, `${sessionId}.jsonl`);
2115
+ const logPath = join15(logDir, `${sessionId}.jsonl`);
1928
2116
  const lines = entries.map(
1929
2117
  (e) => JSON.stringify({
1930
2118
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1935,16 +2123,15 @@ function writeAuditLog(brainRoot, sessionId, entries) {
1935
2123
  applied: e.applied
1936
2124
  })
1937
2125
  );
1938
- writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
2126
+ writeFileSync12(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
1939
2127
  }
1940
- var NEGATION_PATTERNS, AFFIRMATION_PATTERNS;
2128
+ var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS;
1941
2129
  var init_digest = __esm({
1942
2130
  "src/digest.ts"() {
1943
2131
  "use strict";
1944
2132
  init_constants();
1945
2133
  init_grow();
1946
2134
  init_episode();
1947
- init_similarity();
1948
2135
  NEGATION_PATTERNS = [
1949
2136
  /\bdon[''\u2019]?t\b/i,
1950
2137
  /\bdo not\b/i,
@@ -1963,13 +2150,24 @@ var init_digest = __esm({
1963
2150
  ];
1964
2151
  AFFIRMATION_PATTERNS = [
1965
2152
  /\balways\b/i,
1966
- /\bmust\b/i,
1967
2153
  /\bshould\s+always\b/i,
1968
2154
  /\buse\s+\w+\s+instead\b/i,
1969
2155
  // Korean affirmation
1970
- /항상/,
2156
+ /항상/
2157
+ ];
2158
+ MUST_PATTERNS = [
2159
+ /\bmust\b/i,
2160
+ /\brequired\b/i,
2161
+ // Korean
1971
2162
  /반드시/
1972
2163
  ];
2164
+ WARN_PATTERNS = [
2165
+ /\bcareful\b/i,
2166
+ /\bwatch\s+out\b/i,
2167
+ /\bwarning\b/i,
2168
+ // Korean
2169
+ /주의/
2170
+ ];
1973
2171
  }
1974
2172
  });
1975
2173
 
@@ -2075,6 +2273,12 @@ async function main(argv) {
2075
2273
  }
2076
2274
  const { emitToTarget: emitToTarget2 } = await Promise.resolve().then(() => (init_emit(), emit_exports));
2077
2275
  await emitToTarget2(brainRoot, target);
2276
+ const { checkForUpdates: checkForUpdates2, formatUpdateBanner: formatUpdateBanner2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2277
+ checkForUpdates2(VERSION).then((status) => {
2278
+ const banner = formatUpdateBanner2(status);
2279
+ if (banner) console.error(banner);
2280
+ }).catch(() => {
2281
+ });
2078
2282
  break;
2079
2283
  }
2080
2284
  case "fire": {
@@ -2164,9 +2368,15 @@ async function main(argv) {
2164
2368
  case "uninstall":
2165
2369
  uninstallHooks2();
2166
2370
  break;
2167
- case "status":
2371
+ case "status": {
2168
2372
  checkHooks2();
2373
+ console.log(` version: v${VERSION}`);
2374
+ const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2375
+ const updateStatus = await checkUpdates(VERSION);
2376
+ const updateBanner = formatBanner(updateStatus);
2377
+ if (updateBanner) console.error(updateBanner);
2169
2378
  break;
2379
+ }
2170
2380
  default:
2171
2381
  console.error("Usage: hebbian claude <install|uninstall|status>");
2172
2382
  process.exit(1);