@evomap/evolver 1.88.1 → 1.88.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.
- package/index.js +159 -3
- package/package.json +2 -1
- package/src/adapters/claudeCode.js +21 -1
- package/src/adapters/hookAdapter.js +4 -2
- package/src/adapters/scripts/_runtimePaths.js +160 -36
- package/src/adapters/scripts/evolver-session-start.js +14 -10
- package/src/adapters/scripts/evolver-task-recall.js +173 -0
- package/src/atp/atpExecute.js +20 -7
- package/src/atp/cli.js +17 -9
- package/src/atp/protocol.js +41 -0
- package/src/config.js +29 -0
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/forceUpdate.js +108 -3
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetCallLog.js +40 -1
- package/src/gep/autoDistillConv.js +1 -1
- package/src/gep/autoDistillLlm.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationSniffer.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/execBridge.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -0
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +5 -0
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/extensions/traceControl.js +1 -0
- package/src/proxy/index.js +46 -4
- package/src/proxy/inject.js +1 -0
- package/src/proxy/lifecycle/manager.js +457 -2
- package/src/proxy/mailbox/store.js +1 -0
- package/src/proxy/router/messages_route.js +57 -8
- package/src/proxy/trace/extractor.js +1 -0
package/index.js
CHANGED
|
@@ -1185,6 +1185,17 @@ async function main() {
|
|
|
1185
1185
|
hubUrl: process.env.A2A_HUB_URL,
|
|
1186
1186
|
});
|
|
1187
1187
|
console.log('[Proxy] Started on ' + proxyInfo.url);
|
|
1188
|
+
try {
|
|
1189
|
+
const { injectProxyEnv } = require('./src/proxy/inject');
|
|
1190
|
+
const injected = injectProxyEnv(proxyInfo);
|
|
1191
|
+
if (injected.injected) {
|
|
1192
|
+
console.log('[Proxy] Auto-injected client env for Claude Code/Codex/Cursor. Set EVOMAP_PROXY_AUTO_INJECT=off to disable.');
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log('[Proxy] Auto-inject skipped: ' + injected.reason);
|
|
1195
|
+
}
|
|
1196
|
+
} catch (injectErr) {
|
|
1197
|
+
console.warn('[Proxy] Auto-inject failed: ' + (injectErr && injectErr.message || injectErr));
|
|
1198
|
+
}
|
|
1188
1199
|
const { registerMailboxTransport } = require('./src/gep/mailboxTransport');
|
|
1189
1200
|
registerMailboxTransport();
|
|
1190
1201
|
process.env.A2A_TRANSPORT = 'mailbox';
|
|
@@ -1760,8 +1771,8 @@ async function main() {
|
|
|
1760
1771
|
console.error('[exec] --max-cycles requires a value (e.g. --max-cycles 5 or --max-cycles=5)');
|
|
1761
1772
|
process.exit(2);
|
|
1762
1773
|
}
|
|
1763
|
-
if (!['claude-code', 'openclaw'].includes(harness)) {
|
|
1764
|
-
console.error(`[exec] unknown --harness '${harness}' (expected claude-code | openclaw)`);
|
|
1774
|
+
if (!['claude-code', 'openclaw', 'codex', 'opencode'].includes(harness)) {
|
|
1775
|
+
console.error(`[exec] unknown --harness '${harness}' (expected claude-code | openclaw | codex | opencode)`);
|
|
1765
1776
|
process.exit(2);
|
|
1766
1777
|
}
|
|
1767
1778
|
try {
|
|
@@ -2763,8 +2774,153 @@ async function main() {
|
|
|
2763
2774
|
process.exit(1);
|
|
2764
2775
|
}
|
|
2765
2776
|
|
|
2777
|
+
} else if (command === 'recipe') {
|
|
2778
|
+
// recipe build — assemble a DNA blueprint from owned Gene/Capsule assets
|
|
2779
|
+
// recipe reuse — fetch + express an existing recipe into an organism
|
|
2780
|
+
const sub = args[1];
|
|
2781
|
+
const {
|
|
2782
|
+
getHubUrl, getNodeId, getHubNodeSecret, sendHelloToHub, rotateNodeSecret,
|
|
2783
|
+
hubCreateRecipe, hubPublishRecipe, hubGetRecipe, hubExpressRecipe,
|
|
2784
|
+
} = require('./src/gep/a2aProtocol');
|
|
2785
|
+
|
|
2786
|
+
const hubUrl = getHubUrl();
|
|
2787
|
+
if (!hubUrl) {
|
|
2788
|
+
console.error('[recipe] A2A_HUB_URL is not configured. Set A2A_HUB_URL (e.g. https://evomap.ai).');
|
|
2789
|
+
process.exit(1);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function flagVal(name) {
|
|
2793
|
+
const eq = args.find(a => typeof a === 'string' && a.startsWith('--' + name + '='));
|
|
2794
|
+
return eq ? eq.split('=').slice(1).join('=') : null;
|
|
2795
|
+
}
|
|
2796
|
+
async function ensureRegistered(tag) {
|
|
2797
|
+
if (!getHubNodeSecret()) {
|
|
2798
|
+
console.log('[' + tag + '] No node_secret found. Registering with Hub...');
|
|
2799
|
+
const hello = await sendHelloToHub();
|
|
2800
|
+
if (!hello || !hello.ok) {
|
|
2801
|
+
console.error('[' + tag + '] Failed to register with Hub:', (hello && hello.error) || 'unknown');
|
|
2802
|
+
process.exit(1);
|
|
2803
|
+
}
|
|
2804
|
+
console.log('[' + tag + '] Registered as ' + getNodeId());
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
// True when the hub rejected our node_secret as stale/invalid — the one
|
|
2808
|
+
// case where a rotate-and-retry is the documented recovery.
|
|
2809
|
+
function isStaleSecret(result) {
|
|
2810
|
+
if (!result || result.ok) return false;
|
|
2811
|
+
if (result.status !== 401 && result.status !== 403) return false;
|
|
2812
|
+
const e = String(result.error || '');
|
|
2813
|
+
return e.includes('node_secret_invalid') || e.includes('node_secret_not_set');
|
|
2814
|
+
}
|
|
2815
|
+
// Run a hub call; if it fails because our node_secret is stale, rotate
|
|
2816
|
+
// once and retry. Rotation only works when the CURRENT secret is still
|
|
2817
|
+
// server-valid (the hub authenticates the rotate with it). If the secret
|
|
2818
|
+
// has fully diverged from the server, rotation cannot recover it — that
|
|
2819
|
+
// requires re-registering, so we surface the actionable recovery path
|
|
2820
|
+
// instead of silently looping.
|
|
2821
|
+
let _authRecoveryFailed = false;
|
|
2822
|
+
async function callWithAuthRetry(tag, fn) {
|
|
2823
|
+
let result = await fn();
|
|
2824
|
+
if (isStaleSecret(result) && typeof rotateNodeSecret === 'function' && !_authRecoveryFailed) {
|
|
2825
|
+
console.log('[' + tag + '] node_secret stale; rotating via /a2a/hello and retrying...');
|
|
2826
|
+
const rot = await rotateNodeSecret();
|
|
2827
|
+
if (rot && rot.ok) {
|
|
2828
|
+
result = await fn();
|
|
2829
|
+
} else {
|
|
2830
|
+
_authRecoveryFailed = true;
|
|
2831
|
+
console.error('[' + tag + '] Could not auto-rotate: the local node_secret has diverged from the Hub and can no longer authenticate a rotate.');
|
|
2832
|
+
console.error(' Recover by either:');
|
|
2833
|
+
console.error(' 1. Reset Secret on the web (Account -> Reset Secret), then run: node index.js reset-local-secret');
|
|
2834
|
+
console.error(' 2. Or register a fresh node: set a new A2A_NODE_ID and retry (auto-provisions).');
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
return result;
|
|
2838
|
+
}
|
|
2839
|
+
function reportHubError(tag, result) {
|
|
2840
|
+
console.error('[' + tag + '] Hub call failed' + (result.status ? ' (HTTP ' + result.status + ')' : '') + ': ' + (result.error || 'unknown'));
|
|
2841
|
+
if (result.status === 401 || result.status === 403) console.error(' Auth failed. If this persists, delete ~/.evomap/node_secret and retry.');
|
|
2842
|
+
else if (result.status === 402) console.error(' Insufficient credits. Check your balance at ' + hubUrl);
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
if (sub === 'build') {
|
|
2846
|
+
// --genes=<asset_id,...> ordered; types resolved from the local asset store.
|
|
2847
|
+
const genesArg = flagVal('genes');
|
|
2848
|
+
const title = flagVal('title');
|
|
2849
|
+
const description = flagVal('description');
|
|
2850
|
+
const doPublish = args.includes('--publish');
|
|
2851
|
+
if (!genesArg || !title) {
|
|
2852
|
+
console.error('Usage: node index.js recipe build --title="..." --genes=<asset_id,...> [--description="..."] [--price=N] [--publish]');
|
|
2853
|
+
console.error(' Builds a DRAFT recipe by default. --publish is opt-in and pushes it live.');
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2856
|
+
const { loadGenes, loadCapsules } = require('./src/gep/assetStore');
|
|
2857
|
+
const typeById = new Map();
|
|
2858
|
+
try {
|
|
2859
|
+
for (const g of (loadGenes() || [])) if (g && g.asset_id) typeById.set(g.asset_id, 'Gene');
|
|
2860
|
+
for (const c of (loadCapsules() || [])) if (c && c.asset_id) typeById.set(c.asset_id, 'Capsule');
|
|
2861
|
+
} catch (e) { /* fall back to Gene below */ }
|
|
2862
|
+
|
|
2863
|
+
const ids = genesArg.split(',').map(s => s.trim()).filter(Boolean);
|
|
2864
|
+
if (ids.length === 0) { console.error('[recipe build] --genes is empty.'); process.exit(1); }
|
|
2865
|
+
if (ids.length > 20) { console.error('[recipe build] at most 20 steps per recipe.'); process.exit(1); }
|
|
2866
|
+
const steps = ids.map((asset_id, i) => ({
|
|
2867
|
+
asset_id,
|
|
2868
|
+
asset_type: typeById.get(asset_id) || 'Gene',
|
|
2869
|
+
position: i,
|
|
2870
|
+
}));
|
|
2871
|
+
|
|
2872
|
+
await ensureRegistered('recipe build');
|
|
2873
|
+
const priceVal = flagVal('price');
|
|
2874
|
+
const createRes = await callWithAuthRetry('recipe build', () => hubCreateRecipe({
|
|
2875
|
+
title, steps, description: description || undefined,
|
|
2876
|
+
pricePerExecution: priceVal ? Number(priceVal) : undefined,
|
|
2877
|
+
}));
|
|
2878
|
+
if (!createRes.ok) { reportHubError('recipe build', createRes); process.exit(1); }
|
|
2879
|
+
const recipe = (createRes.data && (createRes.data.recipe || createRes.data)) || {};
|
|
2880
|
+
const recipeId = recipe.id;
|
|
2881
|
+
console.log('[recipe build] Created DRAFT recipe ' + recipeId + ' ("' + title + '", ' + steps.length + ' steps).');
|
|
2882
|
+
|
|
2883
|
+
if (doPublish && recipeId) {
|
|
2884
|
+
const pubRes = await callWithAuthRetry('recipe build', () => hubPublishRecipe(recipeId));
|
|
2885
|
+
if (!pubRes.ok) { reportHubError('recipe build', pubRes); process.exit(1); }
|
|
2886
|
+
console.log('[recipe build] Published recipe ' + recipeId + ' to the marketplace.');
|
|
2887
|
+
} else if (!doPublish) {
|
|
2888
|
+
console.log('[recipe build] Left as draft. Re-run with --publish to make it live.');
|
|
2889
|
+
}
|
|
2890
|
+
process.exit(0);
|
|
2891
|
+
} else if (sub === 'reuse') {
|
|
2892
|
+
const recipeId = flagVal('id') || (args[2] && !String(args[2]).startsWith('-') ? args[2] : null);
|
|
2893
|
+
if (!recipeId) {
|
|
2894
|
+
console.error('Usage: node index.js recipe reuse --id=<recipe_id> [--input=<json>]');
|
|
2895
|
+
process.exit(1);
|
|
2896
|
+
}
|
|
2897
|
+
await ensureRegistered('recipe reuse');
|
|
2898
|
+
const getRes = await hubGetRecipe(recipeId);
|
|
2899
|
+
if (!getRes.ok) { reportHubError('recipe reuse', getRes); process.exit(1); }
|
|
2900
|
+
let inputPayload = {};
|
|
2901
|
+
const inputArg = flagVal('input');
|
|
2902
|
+
if (inputArg) {
|
|
2903
|
+
try { inputPayload = JSON.parse(inputArg); }
|
|
2904
|
+
catch (e) { console.error('[recipe reuse] --input must be valid JSON.'); process.exit(1); }
|
|
2905
|
+
}
|
|
2906
|
+
const expRes = await callWithAuthRetry('recipe reuse', () => hubExpressRecipe(recipeId, inputPayload));
|
|
2907
|
+
if (!expRes.ok) { reportHubError('recipe reuse', expRes); process.exit(1); }
|
|
2908
|
+
console.log('[recipe reuse] Expressed recipe ' + recipeId + '.');
|
|
2909
|
+
if (isVerbose) console.log(JSON.stringify(expRes.data, null, 2));
|
|
2910
|
+
process.exit(0);
|
|
2911
|
+
} else {
|
|
2912
|
+
console.error('Usage: node index.js recipe <build|reuse> [flags]');
|
|
2913
|
+
console.error(' build --title="..." --genes=<asset_id,...> [--publish] (draft unless --publish)');
|
|
2914
|
+
console.error(' reuse --id=<recipe_id> [--input=<json>]');
|
|
2915
|
+
process.exit(1);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2766
2918
|
} else {
|
|
2767
|
-
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp|atp-complete] [--loop]
|
|
2919
|
+
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|recipe|buy|orders|verify|atp|atp-complete] [--loop]
|
|
2920
|
+
- recipe flags:
|
|
2921
|
+
- build --title="..." --genes=<asset_id,...> [--description] [--price=N] [--publish]
|
|
2922
|
+
(builds a DRAFT DNA blueprint; --publish is opt-in)
|
|
2923
|
+
- reuse --id=<recipe_id> [--input=<json>] (express a recipe into an organism)
|
|
2768
2924
|
- fetch flags:
|
|
2769
2925
|
- --skill=<id> | -s <id> (skill ID to download)
|
|
2770
2926
|
- --out=<dir> (output directory, default: ./skills/<skill_id>)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.88.
|
|
3
|
+
"version": "1.88.3",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@aws-sdk/client-bedrock-runtime": "^3.1053.0",
|
|
40
|
+
"@evomap/atp-sdk": "^0.1.0",
|
|
40
41
|
"@evomap/gep-sdk": "^1.5.0",
|
|
41
42
|
"dotenv": "^16.4.7",
|
|
42
43
|
"undici": "^7.0.0"
|
|
@@ -20,6 +20,24 @@ function buildClaudeHooks(evolverRoot) {
|
|
|
20
20
|
],
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
|
+
UserPromptSubmit: [
|
|
24
|
+
{
|
|
25
|
+
hooks: [
|
|
26
|
+
{
|
|
27
|
+
type: 'command',
|
|
28
|
+
// Runtime asset injection (P4-c). DEFAULT off (EVOLVER_RECALL_MODE
|
|
29
|
+
// unset/off -> emits {} without reading the prompt). The script
|
|
30
|
+
// owns an absolute 3.3s watchdog that always exits 0 with valid
|
|
31
|
+
// JSON (fail-open); this host timeout (5s) is a strict backstop
|
|
32
|
+
// ABOVE that watchdog, so the host never kills the script
|
|
33
|
+
// mid-write. A stuck/slow recall can never block or erase the
|
|
34
|
+
// user's prompt.
|
|
35
|
+
command: `node ${scriptsBase}/evolver-task-recall.js`,
|
|
36
|
+
timeout: 5,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
23
41
|
PostToolUse: [
|
|
24
42
|
{
|
|
25
43
|
matcher: 'Write',
|
|
@@ -55,6 +73,8 @@ This project uses evolver for self-evolution. Hooks automatically:
|
|
|
55
73
|
1. Inject recent evolution memory at session start
|
|
56
74
|
2. Detect evolution signals during file edits
|
|
57
75
|
3. Record outcomes at session end
|
|
76
|
+
4. (Opt-in) Surface matching distilled capabilities for each prompt — set
|
|
77
|
+
\`EVOLVER_RECALL_MODE=shadow\` to preview, \`enforce\` to inject (default off).
|
|
58
78
|
|
|
59
79
|
For substantive tasks, call \`gep_recall\` before work and \`gep_record_outcome\` after.
|
|
60
80
|
Signals: log_error, perf_bottleneck, user_feature_request, capability_gap, deployment_issue, test_failure.`;
|
|
@@ -126,7 +146,7 @@ function uninstall({ configRoot }) {
|
|
|
126
146
|
const innerBefore = matcher.hooks.length;
|
|
127
147
|
const filtered = matcher.hooks.filter(h => {
|
|
128
148
|
const cmd = (h && h.command) || '';
|
|
129
|
-
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
149
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
|
|
130
150
|
});
|
|
131
151
|
// A matcher containing both evolver and user hooks shrinks
|
|
132
152
|
// its inner array without changing the outer matcher count.
|
|
@@ -76,7 +76,7 @@ function mergeWithHooksUnion(target, source) {
|
|
|
76
76
|
if (tArr && sArr) {
|
|
77
77
|
const isEvolverOwned = (entry) => {
|
|
78
78
|
const cmds = collectCommands(entry);
|
|
79
|
-
return cmds.some(c => c.includes('evolver-session') || c.includes('evolver-signal'));
|
|
79
|
+
return cmds.some(c => c.includes('evolver-session') || c.includes('evolver-signal') || c.includes('evolver-task-recall'));
|
|
80
80
|
};
|
|
81
81
|
const userEntries = tArr.filter(e => !isEvolverOwned(e));
|
|
82
82
|
result.hooks[event] = [...userEntries, ...sArr];
|
|
@@ -171,6 +171,7 @@ function copyHookScripts(destDir, evolverRoot) {
|
|
|
171
171
|
'evolver-session-start.js',
|
|
172
172
|
'evolver-signal-detect.js',
|
|
173
173
|
'evolver-session-end.js',
|
|
174
|
+
'evolver-task-recall.js',
|
|
174
175
|
];
|
|
175
176
|
fs.mkdirSync(destDir, { recursive: true });
|
|
176
177
|
const copied = [];
|
|
@@ -224,7 +225,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
|
|
|
224
225
|
const before = data.hooks[event].length;
|
|
225
226
|
data.hooks[event] = data.hooks[event].filter(h => {
|
|
226
227
|
const cmd = h.command || '';
|
|
227
|
-
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
228
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
|
|
228
229
|
});
|
|
229
230
|
if (data.hooks[event].length !== before) changed = true;
|
|
230
231
|
if (data.hooks[event].length === 0) delete data.hooks[event];
|
|
@@ -257,6 +258,7 @@ function removeHookScripts(hooksDir) {
|
|
|
257
258
|
'evolver-session-start.js',
|
|
258
259
|
'evolver-signal-detect.js',
|
|
259
260
|
'evolver-session-end.js',
|
|
261
|
+
'evolver-task-recall.js',
|
|
260
262
|
];
|
|
261
263
|
let removed = 0;
|
|
262
264
|
for (const name of scripts) {
|
|
@@ -31,6 +31,137 @@ function isEvolverPackageJson(filePath) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Scan a "versions dir" used by Node version managers (NVM, fnm, Volta, asdf)
|
|
35
|
+
// and append each `<versions-dir>/<version>/<subdir>/node_modules` to `out`.
|
|
36
|
+
// Skips silently when the versions dir does not exist (typical case — most
|
|
37
|
+
// users have at most one version manager). Most-recent-mtime first so the
|
|
38
|
+
// active version is preferred when a user has multiple Node versions
|
|
39
|
+
// installed.
|
|
40
|
+
function _scanVersionedNodeModules(versionsDir, subdir, out) {
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = fs.readdirSync(versionsDir, { withFileTypes: true });
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const dirs = entries
|
|
48
|
+
.filter((e) => e.isDirectory && e.isDirectory())
|
|
49
|
+
.map((e) => {
|
|
50
|
+
const full = path.join(versionsDir, e.name);
|
|
51
|
+
let mtime = 0;
|
|
52
|
+
try { mtime = fs.statSync(full).mtimeMs; } catch {}
|
|
53
|
+
return { full, mtime };
|
|
54
|
+
})
|
|
55
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
56
|
+
for (const d of dirs) {
|
|
57
|
+
out.push(path.join(d.full, subdir, 'node_modules'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build the require.resolve paths array. All entries are user/system-scoped
|
|
62
|
+
// install roots — process.cwd() is intentionally excluded for the same
|
|
63
|
+
// prompt-injection reason as the original allowlist (see comment in
|
|
64
|
+
// findEvolverRoot below).
|
|
65
|
+
function _buildInstallSearchPaths() {
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
// Env-derived bases must be ABSOLUTE. A relative override (e.g. NVM_DIR='.nvm')
|
|
68
|
+
// or empty value would resolve against process.cwd() and let a hostile
|
|
69
|
+
// workspace plant a fake @evomap/evolver in the require.resolve allowlist —
|
|
70
|
+
// the prompt-injection surface PR #94 closed. isAbsolute is platform-matched
|
|
71
|
+
// (path.win32 recognises C:\ ...) so the guard holds on Windows too; a
|
|
72
|
+
// non-absolute override falls through to the trusted home/system default.
|
|
73
|
+
const _pathFlavor = process.platform === 'win32' ? path.win32 : path.posix;
|
|
74
|
+
const absEnv = (v) => (v && _pathFlavor.isAbsolute(v)) ? v : null;
|
|
75
|
+
const paths = [
|
|
76
|
+
// npm global with `npm config set prefix` overrides
|
|
77
|
+
path.join(home, '.npm-global', 'lib', 'node_modules'),
|
|
78
|
+
path.join(home, '.local', 'lib', 'node_modules'),
|
|
79
|
+
// System-wide (apt/yum nodejs, Intel Mac Homebrew)
|
|
80
|
+
'/usr/lib/node_modules',
|
|
81
|
+
'/usr/local/lib/node_modules',
|
|
82
|
+
// Apple Silicon Homebrew (default since macOS Big Sur on M1/M2/M3/M4 —
|
|
83
|
+
// the majority of Mac dev hardware sold since 2021). Without this
|
|
84
|
+
// entry, `npm install -g @evomap/evolver` on an Apple Silicon Mac
|
|
85
|
+
// lands at /opt/homebrew/lib/node_modules/@evomap/evolver and the
|
|
86
|
+
// hook scripts cannot find the package -> additionalContext is empty
|
|
87
|
+
// -> evolution memory never reaches the LLM.
|
|
88
|
+
'/opt/homebrew/lib/node_modules',
|
|
89
|
+
// Linuxbrew (Homebrew on Linux — niche but real).
|
|
90
|
+
'/home/linuxbrew/.linuxbrew/lib/node_modules',
|
|
91
|
+
];
|
|
92
|
+
// Per-user Node version managers. Each manager has its own on-disk layout
|
|
93
|
+
// and its own base-dir env override; the version subdirectory is dynamic
|
|
94
|
+
// (e.g. `~/.nvm/versions/node/v22.15.0`) so we scan and append each
|
|
95
|
+
// version's node_modules. These were missing from the original hard-coded
|
|
96
|
+
// list even though NVM in particular is extremely common across all OSes.
|
|
97
|
+
|
|
98
|
+
// NVM. Globals are per-version under `<NVM_DIR>/versions/node/<ver>/lib`.
|
|
99
|
+
// NVM_DIR defaults to ~/.nvm but is frequently relocated.
|
|
100
|
+
const nvmDir = absEnv(process.env.NVM_DIR) || path.join(home, '.nvm');
|
|
101
|
+
_scanVersionedNodeModules(path.join(nvmDir, 'versions', 'node'), 'lib', paths);
|
|
102
|
+
|
|
103
|
+
// fnm. Each version lives under `<base>/node-versions/<ver>/installation/`,
|
|
104
|
+
// and fnm does NOT override the npm prefix, so globals are at
|
|
105
|
+
// `installation/lib/node_modules`. The base dir is XDG-first
|
|
106
|
+
// (`$XDG_DATA_HOME/fnm`, i.e. ~/.local/share/fnm on Linux and
|
|
107
|
+
// ~/Library/Application Support/fnm on macOS); `~/.fnm` is only the legacy
|
|
108
|
+
// fallback. `$FNM_DIR` overrides everything. Scan all candidate bases;
|
|
109
|
+
// _scanVersionedNodeModules silently skips the ones that don't exist.
|
|
110
|
+
const fnmSub = path.join('installation', 'lib');
|
|
111
|
+
const fnmBases = [];
|
|
112
|
+
if (absEnv(process.env.FNM_DIR)) fnmBases.push(process.env.FNM_DIR);
|
|
113
|
+
if (absEnv(process.env.XDG_DATA_HOME)) fnmBases.push(path.join(process.env.XDG_DATA_HOME, 'fnm'));
|
|
114
|
+
fnmBases.push(path.join(home, '.local', 'share', 'fnm')); // Linux XDG default
|
|
115
|
+
fnmBases.push(path.join(home, 'Library', 'Application Support', 'fnm')); // macOS default
|
|
116
|
+
fnmBases.push(path.join(home, '.fnm')); // legacy
|
|
117
|
+
for (const base of fnmBases) {
|
|
118
|
+
_scanVersionedNodeModules(path.join(base, 'node-versions'), fnmSub, paths);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Volta does NOT store global packages alongside the Node image. It
|
|
122
|
+
// sandboxes each `npm install -g`'d package under
|
|
123
|
+
// `<VOLTA_HOME>/tools/image/packages/<name>/lib/node_modules` (the scope
|
|
124
|
+
// becomes a real nested directory). Because we know the package name, this
|
|
125
|
+
// is a single fixed path rather than a version scan. VOLTA_HOME defaults to
|
|
126
|
+
// ~/.volta on macOS/Linux but to %LOCALAPPDATA%\Volta on Windows (where the
|
|
127
|
+
// globals actually live, and hook processes often don't inherit VOLTA_HOME).
|
|
128
|
+
// Verified against volta-cli/volta `volta-layout` v4 (`package_image_dir`) +
|
|
129
|
+
// `package/manager.rs` (`source_dir` = lib/node_modules).
|
|
130
|
+
const voltaHome = absEnv(process.env.VOLTA_HOME)
|
|
131
|
+
|| (process.platform === 'win32'
|
|
132
|
+
? path.join(absEnv(process.env.LOCALAPPDATA) || path.join(home, 'AppData', 'Local'), 'Volta')
|
|
133
|
+
: path.join(home, '.volta'));
|
|
134
|
+
paths.push(path.join(voltaHome, 'tools', 'image', 'packages', '@evomap', 'evolver', 'lib', 'node_modules'));
|
|
135
|
+
|
|
136
|
+
// asdf. Globals are per-version under `<data>/installs/nodejs/<ver>/`.
|
|
137
|
+
// asdf-nodejs dropped the `.npm` prefix override in PR #228 (Sept 2022),
|
|
138
|
+
// so modern installs use plain `lib/node_modules`; older installs (never
|
|
139
|
+
// re-created) still use `.npm/lib/node_modules`. Scan both. `$ASDF_DATA_DIR`
|
|
140
|
+
// overrides the ~/.asdf default (asdf 0.16+ Go rewrite).
|
|
141
|
+
const asdfData = absEnv(process.env.ASDF_DATA_DIR) || path.join(home, '.asdf');
|
|
142
|
+
const asdfVersions = path.join(asdfData, 'installs', 'nodejs');
|
|
143
|
+
_scanVersionedNodeModules(asdfVersions, 'lib', paths); // modern (post-#228)
|
|
144
|
+
_scanVersionedNodeModules(asdfVersions, path.join('.npm', 'lib'), paths); // legacy (pre-#228)
|
|
145
|
+
|
|
146
|
+
// Windows: `npm install -g` puts packages under %APPDATA%\npm\node_modules
|
|
147
|
+
// (most common; same convention as `npm config get prefix` default on win32),
|
|
148
|
+
// %ProgramFiles%\nodejs\node_modules (system-wide installer), or
|
|
149
|
+
// %ProgramFiles(x86)%\nodejs\node_modules (32-bit Node on a 64-bit host).
|
|
150
|
+
// Conditional expansion keeps the POSIX base list untouched on Linux/macOS.
|
|
151
|
+
if (process.platform === 'win32') {
|
|
152
|
+
const appdata = absEnv(process.env.APPDATA) || path.join(home, 'AppData', 'Roaming');
|
|
153
|
+
paths.push(path.join(appdata, 'npm', 'node_modules'));
|
|
154
|
+
if (absEnv(process.env.ProgramFiles)) {
|
|
155
|
+
paths.push(path.join(process.env.ProgramFiles, 'nodejs', 'node_modules'));
|
|
156
|
+
}
|
|
157
|
+
if (absEnv(process.env['ProgramFiles(x86)'])) {
|
|
158
|
+
paths.push(path.join(process.env['ProgramFiles(x86)'], 'nodejs', 'node_modules'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return paths;
|
|
163
|
+
}
|
|
164
|
+
|
|
34
165
|
function findEvolverRoot() {
|
|
35
166
|
if (process.env.EVOLVER_ROOT) {
|
|
36
167
|
const explicit = process.env.EVOLVER_ROOT;
|
|
@@ -57,39 +188,18 @@ function findEvolverRoot() {
|
|
|
57
188
|
// be selected here and control `findMemoryGraph()` -> the memory graph
|
|
58
189
|
// contents become attacker-controlled prompt-injection material in
|
|
59
190
|
// `evolver-session-start.js`'s `additionalContext`. Restrict to trusted,
|
|
60
|
-
// user/system-scoped install roots.
|
|
191
|
+
// user/system-scoped install roots (built in `_buildInstallSearchPaths`).
|
|
61
192
|
try {
|
|
62
|
-
//
|
|
63
|
-
// (
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'npm', 'node_modules'
|
|
71
|
-
),
|
|
72
|
-
...(process.env.ProgramFiles
|
|
73
|
-
? [path.join(process.env.ProgramFiles, 'nodejs', 'node_modules')]
|
|
74
|
-
: []),
|
|
75
|
-
...(process.env['ProgramFiles(x86)']
|
|
76
|
-
? [path.join(process.env['ProgramFiles(x86)'], 'nodejs', 'node_modules')]
|
|
77
|
-
: []),
|
|
78
|
-
]
|
|
79
|
-
: [];
|
|
80
|
-
|
|
193
|
+
// Allowlist of trusted user/system-scoped install roots. Built by
|
|
194
|
+
// _buildInstallSearchPaths() above so the list is one source of truth
|
|
195
|
+
// (Apple Silicon Homebrew, Linuxbrew, NVM / fnm / Volta / asdf,
|
|
196
|
+
// and Windows %APPDATA%\npm + %ProgramFiles%\nodejs install layouts).
|
|
197
|
+
// process.cwd() is intentionally excluded: a hostile workspace can plant
|
|
198
|
+
// its own node_modules/@evomap/evolver/package.json which would then
|
|
199
|
+
// control findMemoryGraph() and feed attacker-controlled content into
|
|
200
|
+
// evolver-session-start.js's additionalContext.
|
|
81
201
|
const pkgJson = require.resolve('@evomap/evolver/package.json', {
|
|
82
|
-
|
|
83
|
-
// node_modules/@evomap/evolver to gain control over the memory graph path
|
|
84
|
-
// (prompt-injection surface: evolver-session-start.js additionalContext).
|
|
85
|
-
// Only trust user/system-scoped install roots.
|
|
86
|
-
paths: [
|
|
87
|
-
path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),
|
|
88
|
-
path.join(os.homedir(), '.local', 'lib', 'node_modules'),
|
|
89
|
-
'/usr/lib/node_modules',
|
|
90
|
-
'/usr/local/lib/node_modules',
|
|
91
|
-
..._winPaths,
|
|
92
|
-
],
|
|
202
|
+
paths: _buildInstallSearchPaths(),
|
|
93
203
|
});
|
|
94
204
|
if (pkgJson && isEvolverPackageJson(pkgJson)) {
|
|
95
205
|
return path.dirname(pkgJson);
|
|
@@ -293,10 +403,10 @@ function findMemoryGraph(evolverRoot) {
|
|
|
293
403
|
}
|
|
294
404
|
|
|
295
405
|
// Is `dir` inside a git work tree? Cheap, no-shell `git rev-parse`. Returns
|
|
296
|
-
// false on any error (git missing, not a repo, timeout) and never throws
|
|
297
|
-
// session-start hook uses this only to decide whether to surface a
|
|
298
|
-
// "evolver needs a git workspace" notice, so a false negative just
|
|
299
|
-
// the notice rather than breaking anything.
|
|
406
|
+
// false on any error (git missing, not a repo, timeout) and never throws.
|
|
407
|
+
// The session-start hook uses this only to decide whether to surface a
|
|
408
|
+
// one-line "evolver needs a git workspace" notice, so a false negative just
|
|
409
|
+
// suppresses the notice rather than breaking anything.
|
|
300
410
|
function isGitWorkspace(dir) {
|
|
301
411
|
try {
|
|
302
412
|
const res = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
@@ -312,4 +422,18 @@ function isGitWorkspace(dir) {
|
|
|
312
422
|
}
|
|
313
423
|
}
|
|
314
424
|
|
|
315
|
-
module.exports = {
|
|
425
|
+
module.exports = {
|
|
426
|
+
findEvolverRoot,
|
|
427
|
+
findMemoryGraph,
|
|
428
|
+
resolveProjectDir,
|
|
429
|
+
resolveWorkspaceId,
|
|
430
|
+
isGitWorkspace,
|
|
431
|
+
// Test-only: exposes the install-path builder so the test suite can
|
|
432
|
+
// verify Apple Silicon Homebrew + version-manager (NVM/fnm/Volta/asdf)
|
|
433
|
+
// + Windows %APPDATA%\npm paths are included without going through the
|
|
434
|
+
// full require.resolve chain (which depends on a real filesystem layout).
|
|
435
|
+
__internals: {
|
|
436
|
+
buildInstallSearchPaths: _buildInstallSearchPaths,
|
|
437
|
+
scanVersionedNodeModules: _scanVersionedNodeModules,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
@@ -199,14 +199,18 @@ function getDedupStatePath() {
|
|
|
199
199
|
return path.join(dir, 'session-start-state.json');
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
function getNoticeStatePath() {
|
|
203
|
+
const dir = process.env.EVOLVER_SESSION_STATE_DIR
|
|
204
|
+
|| path.join(os.homedir(), '.evolver');
|
|
205
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
206
|
+
return path.join(dir, 'session-start-notice-state.json');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// TTL throttle keyed by an arbitrary string. Returns true if `key` fired within
|
|
210
|
+
// the last `ttlMs` (caller should suppress); otherwise records "now" for `key`
|
|
211
|
+
// and returns false. Best-effort: a state read/write failure just means no
|
|
212
|
+
// throttling (fail open). Entries older than 24h are pruned on write.
|
|
213
|
+
function throttled(key, ttlMs, statePath) {
|
|
210
214
|
let state = {};
|
|
211
215
|
try {
|
|
212
216
|
if (fs.existsSync(statePath)) {
|
|
@@ -241,7 +245,7 @@ function shouldSkipInjection() {
|
|
|
241
245
|
if (!dedupEnabled) return false;
|
|
242
246
|
|
|
243
247
|
const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
|
|
244
|
-
return throttled(process.cwd(), ttlMs);
|
|
248
|
+
return throttled(process.cwd(), ttlMs, getDedupStatePath());
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
function main() {
|
|
@@ -256,7 +260,7 @@ function main() {
|
|
|
256
260
|
// from git diffs), so tell the user — once per folder per TTL — instead of
|
|
257
261
|
// failing silently. Emitted regardless of whether any memory exists below.
|
|
258
262
|
const parts = [];
|
|
259
|
-
if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS)) {
|
|
263
|
+
if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS, getNoticeStatePath())) {
|
|
260
264
|
parts.push(NON_GIT_NOTICE);
|
|
261
265
|
}
|
|
262
266
|
|