hebbian 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AAgBpF,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;CACjD;AAqPD;;GAEG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,SAAO,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CA4BxF;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,EAAE,CAEjD;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AAgBpF,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;CACjD;AAgQD;;GAEG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,SAAO,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CA4BxF;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,EAAE,CAEjD;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC"}
@@ -876,6 +876,9 @@ function growNeuron(brainRoot, neuronPath) {
876
876
  const counter = fireNeuron(brainRoot, neuronPath);
877
877
  return { action: "fired", path: neuronPath, counter };
878
878
  }
879
+ if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
880
+ throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
881
+ }
879
882
  const parts = neuronPath.split("/");
880
883
  const regionName = parts[0];
881
884
  if (!REGIONS.includes(regionName)) {
@@ -1645,7 +1648,16 @@ function error(res, message, status = 400) {
1645
1648
  async function readBody(req) {
1646
1649
  return new Promise((resolve4, reject) => {
1647
1650
  const chunks = [];
1648
- req.on("data", (chunk) => chunks.push(chunk));
1651
+ let total = 0;
1652
+ req.on("data", (chunk) => {
1653
+ total += chunk.length;
1654
+ if (total > MAX_BODY_BYTES) {
1655
+ reject(new Error("Request body too large"));
1656
+ req.destroy();
1657
+ return;
1658
+ }
1659
+ chunks.push(chunk);
1660
+ });
1649
1661
  req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1650
1662
  req.on("error", reject);
1651
1663
  });
@@ -1835,7 +1847,7 @@ function getPendingReports() {
1835
1847
  function clearReports() {
1836
1848
  pendingReports.length = 0;
1837
1849
  }
1838
- var lastAPIActivity, pendingReports;
1850
+ var lastAPIActivity, pendingReports, MAX_BODY_BYTES;
1839
1851
  var init_api = __esm({
1840
1852
  "src/api.ts"() {
1841
1853
  "use strict";
@@ -1852,6 +1864,7 @@ var init_api = __esm({
1852
1864
  init_constants();
1853
1865
  lastAPIActivity = Date.now();
1854
1866
  pendingReports = [];
1867
+ MAX_BODY_BYTES = 1048576;
1855
1868
  }
1856
1869
  });
1857
1870
 
@@ -2145,6 +2158,8 @@ function extractCorrections(messages) {
2145
2158
  if (text.length < MIN_CORRECTION_LENGTH) continue;
2146
2159
  if (/^[\/!]/.test(text.trim())) continue;
2147
2160
  if (text.trim().endsWith("?")) continue;
2161
+ if (/^<[a-zA-Z]/.test(text.trim())) continue;
2162
+ if (/^Base directory for this skill:/i.test(text.trim())) continue;
2148
2163
  const correction = detectCorrection(text);
2149
2164
  if (correction) {
2150
2165
  corrections.push(correction);
@@ -2556,8 +2571,9 @@ function buildOutcomeSummary(brainRoot) {
2556
2571
  });
2557
2572
  for (const [neuron, s] of sorted) {
2558
2573
  const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
2559
- const trend = parseFloat(ratio) > 0.5 ? "\u2190 act on this" : parseFloat(ratio) > 0.3 ? "\u2190 watch" : "";
2560
- lines.push(`- ${neuron}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
2574
+ const trend = parseFloat(ratio) > 0.5 ? "act on this" : parseFloat(ratio) > 0.3 ? "watch" : "";
2575
+ const safePath = neuron.replace(/[\n\r#]/g, " ").trim();
2576
+ lines.push(`- ${safePath}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
2561
2577
  }
2562
2578
  lines.push("");
2563
2579
  return lines.join("\n");
@@ -2621,12 +2637,27 @@ __export(evolve_exports, {
2621
2637
  runEvolve: () => runEvolve,
2622
2638
  validateActions: () => validateActions
2623
2639
  });
2640
+ import { existsSync as existsSync17, readFileSync as readFileSync9, writeFileSync as writeFileSync14 } from "fs";
2641
+ import { join as join18 } from "path";
2624
2642
  async function runEvolve(brainRoot, dryRun) {
2625
2643
  const apiKey = process.env.GEMINI_API_KEY;
2626
2644
  if (!apiKey) {
2627
2645
  console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2628
2646
  return { actions: [], executed: 0, skipped: 0, dryRun };
2629
2647
  }
2648
+ if (!dryRun && process.env.EVOLVE_NO_COOLDOWN !== "1") {
2649
+ const cooldownMs = (parseInt(process.env.EVOLVE_COOLDOWN_SECONDS ?? "60", 10) || 60) * 1e3;
2650
+ const cooldownPath = join18(brainRoot, EVOLVE_COOLDOWN_FILE);
2651
+ if (existsSync17(cooldownPath)) {
2652
+ const lastRun = parseInt(readFileSync9(cooldownPath, "utf8").trim(), 10);
2653
+ const elapsed = Date.now() - lastRun;
2654
+ if (elapsed < cooldownMs) {
2655
+ const remaining = Math.ceil((cooldownMs - elapsed) / 1e3);
2656
+ console.log(`\u23F3 evolve cooldown: ${remaining}s remaining (use EVOLVE_NO_COOLDOWN=1 to bypass)`);
2657
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2658
+ }
2659
+ }
2660
+ }
2630
2661
  const episodes = readEpisodes(brainRoot);
2631
2662
  const brain = scanBrain(brainRoot);
2632
2663
  const summary = buildBrainSummary(brain);
@@ -2657,6 +2688,7 @@ async function runEvolve(brainRoot, dryRun) {
2657
2688
  const executed = executeActions(brainRoot, actions);
2658
2689
  logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
2659
2690
  console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
2691
+ writeFileSync14(join18(brainRoot, EVOLVE_COOLDOWN_FILE), String(Date.now()), "utf8");
2660
2692
  return { actions, executed, skipped, dryRun: false };
2661
2693
  }
2662
2694
  function buildBrainSummary(brain) {
@@ -2679,8 +2711,12 @@ function buildBrainSummary(brain) {
2679
2711
  }
2680
2712
  return lines.join("\n");
2681
2713
  }
2714
+ function sanitizeForPrompt(text) {
2715
+ const firstLine = (text.split("\n")[0] ?? "").trim();
2716
+ return firstLine.replace(/^#+\s*/g, "").slice(0, 200);
2717
+ }
2682
2718
  function buildPrompt(summary, episodes, outcomeSummary) {
2683
- const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2719
+ const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${sanitizeForPrompt(e.detail)}`).join("\n") : "(no recent episodes)";
2684
2720
  const outcomeSection = outcomeSummary || "";
2685
2721
  return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2686
2722
 
@@ -2719,7 +2755,7 @@ Respond with a JSON array of actions:
2719
2755
  }
2720
2756
  async function callGemini(prompt, apiKey) {
2721
2757
  const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2722
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2758
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
2723
2759
  const body = {
2724
2760
  contents: [{ parts: [{ text: prompt }] }],
2725
2761
  generationConfig: {
@@ -2735,7 +2771,7 @@ async function callGemini(prompt, apiKey) {
2735
2771
  try {
2736
2772
  const res = await fetch(url, {
2737
2773
  method: "POST",
2738
- headers: { "Content-Type": "application/json" },
2774
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
2739
2775
  body: JSON.stringify(body),
2740
2776
  signal: AbortSignal.timeout(API_TIMEOUT)
2741
2777
  });
@@ -2789,6 +2825,10 @@ function parseActions(text) {
2789
2825
  }
2790
2826
  function validateActions(actions, _brain) {
2791
2827
  return actions.filter((action) => {
2828
+ if (action.path.includes("..") || action.path.startsWith("/")) {
2829
+ console.log(` \u26A0\uFE0F blocked: ${action.type} ${action.path} (path traversal)`);
2830
+ return false;
2831
+ }
2792
2832
  const region = action.path.split("/")[0];
2793
2833
  if (!region || PROTECTED_REGIONS.includes(region)) {
2794
2834
  console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
@@ -2850,7 +2890,7 @@ function actionIcon(type) {
2850
2890
  return "\u2753";
2851
2891
  }
2852
2892
  }
2853
- var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
2893
+ var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY, EVOLVE_COOLDOWN_FILE;
2854
2894
  var init_evolve = __esm({
2855
2895
  "src/evolve.ts"() {
2856
2896
  "use strict";
@@ -2868,6 +2908,7 @@ var init_evolve = __esm({
2868
2908
  DEFAULT_MODEL = "gemini-2.0-flash-lite";
2869
2909
  API_TIMEOUT = 3e4;
2870
2910
  RETRY_DELAY = 5e3;
2911
+ EVOLVE_COOLDOWN_FILE = "hippocampus/evolve_last_run";
2871
2912
  }
2872
2913
  });
2873
2914
 
@@ -2876,8 +2917,8 @@ var doctor_exports = {};
2876
2917
  __export(doctor_exports, {
2877
2918
  runDoctor: () => runDoctor
2878
2919
  });
2879
- import { existsSync as existsSync17, readFileSync as readFileSync9, readdirSync as readdirSync11 } from "fs";
2880
- import { join as join18 } from "path";
2920
+ import { existsSync as existsSync18, readFileSync as readFileSync10, readdirSync as readdirSync11 } from "fs";
2921
+ import { join as join19 } from "path";
2881
2922
  import { execSync as execSync4 } from "child_process";
2882
2923
  async function runDoctor(brainRoot) {
2883
2924
  let passed = 0, warnings = 0, failed = 0;
@@ -2907,7 +2948,7 @@ async function runDoctor(brainRoot) {
2907
2948
  console.log("\nnpm package");
2908
2949
  try {
2909
2950
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
2910
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
2951
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
2911
2952
  const local = pkg.version || "unknown";
2912
2953
  let remote = "";
2913
2954
  try {
@@ -2924,13 +2965,13 @@ async function runDoctor(brainRoot) {
2924
2965
  warn("Could not read package.json");
2925
2966
  }
2926
2967
  console.log("\nbrain structure");
2927
- if (!existsSync17(brainRoot)) {
2968
+ if (!existsSync18(brainRoot)) {
2928
2969
  fail(`Brain not found at ${brainRoot}`, "hebbian init ./brain");
2929
2970
  } else {
2930
2971
  ok(`Brain root: ${brainRoot}`);
2931
2972
  for (const region of REGIONS) {
2932
- const regionDir = join18(brainRoot, region);
2933
- if (existsSync17(regionDir)) {
2973
+ const regionDir = join19(brainRoot, region);
2974
+ if (existsSync18(regionDir)) {
2934
2975
  ok(`Region: ${region}`);
2935
2976
  } else {
2936
2977
  warn(`Missing region: ${region}`, `mkdir -p ${regionDir}`);
@@ -2938,12 +2979,12 @@ async function runDoctor(brainRoot) {
2938
2979
  }
2939
2980
  }
2940
2981
  console.log("\nClaude Code hooks");
2941
- const settingsPath = join18(process.cwd(), ".claude", "settings.local.json");
2942
- if (!existsSync17(settingsPath)) {
2982
+ const settingsPath = join19(process.cwd(), ".claude", "settings.local.json");
2983
+ if (!existsSync18(settingsPath)) {
2943
2984
  warn("No .claude/settings.local.json found", "hebbian claude install");
2944
2985
  } else {
2945
2986
  try {
2946
- const settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
2987
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
2947
2988
  const hooks = settings.hooks || {};
2948
2989
  const hasStop = Object.entries(hooks).some(
2949
2990
  ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
@@ -2976,8 +3017,8 @@ async function runDoctor(brainRoot) {
2976
3017
  try {
2977
3018
  let total = 0;
2978
3019
  for (const region of REGIONS) {
2979
- const candidateDir = join18(brainRoot, region, "_candidates");
2980
- if (existsSync17(candidateDir)) {
3020
+ const candidateDir = join19(brainRoot, region, "_candidates");
3021
+ if (existsSync18(candidateDir)) {
2981
3022
  const entries = readdirSync11(candidateDir, { withFileTypes: true });
2982
3023
  const count = entries.filter((e) => e.isDirectory()).length;
2983
3024
  total += count;
@@ -3015,7 +3056,7 @@ var init_doctor = __esm({
3015
3056
  init_constants();
3016
3057
  import { parseArgs } from "util";
3017
3058
  import { resolve as resolve3 } from "path";
3018
- var VERSION = "0.5.0";
3059
+ var VERSION = "0.5.2";
3019
3060
  var HELP = `
3020
3061
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
3021
3062