@evomap/evolver 1.89.14 → 1.89.17

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.
Files changed (72) hide show
  1. package/index.js +219 -29
  2. package/package.json +1 -1
  3. package/src/adapters/claudeCode.js +2 -2
  4. package/src/adapters/codex.js +2 -2
  5. package/src/adapters/hookAdapter.js +14 -2
  6. package/src/adapters/scripts/evolver-session-start.js +182 -2
  7. package/src/config.js +11 -3
  8. package/src/evolve/guards.js +1 -1
  9. package/src/evolve/pipeline/collect.js +1 -1
  10. package/src/evolve/pipeline/dispatch.js +1 -1
  11. package/src/evolve/pipeline/enrich.js +1 -1
  12. package/src/evolve/pipeline/hub.js +1 -1
  13. package/src/evolve/pipeline/select.js +1 -1
  14. package/src/evolve/pipeline/signals.js +1 -1
  15. package/src/evolve/utils.js +1 -1
  16. package/src/evolve.js +1 -1
  17. package/src/forceUpdate.js +499 -119
  18. package/src/gep/a2aProtocol.js +1 -1
  19. package/src/gep/antiAbuseTelemetry.js +1 -1
  20. package/src/gep/autoDistillConv.js +1 -1
  21. package/src/gep/autoDistillLlm.js +1 -1
  22. package/src/gep/candidateEval.js +1 -1
  23. package/src/gep/candidates.js +1 -1
  24. package/src/gep/cliContracts.js +1154 -0
  25. package/src/gep/contentHash.js +1 -1
  26. package/src/gep/conversationDistiller.js +1 -1
  27. package/src/gep/conversationSniffer.js +1 -1
  28. package/src/gep/crypto.js +1 -1
  29. package/src/gep/curriculum.js +1 -1
  30. package/src/gep/deviceId.js +1 -1
  31. package/src/gep/envFingerprint.js +1 -1
  32. package/src/gep/epigenetics.js +1 -1
  33. package/src/gep/execBridge.js +1 -1
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hostErrorClassifier.js +34 -0
  37. package/src/gep/hubFetch.js +1 -1
  38. package/src/gep/hubReview.js +1 -1
  39. package/src/gep/hubSearch.js +1 -1
  40. package/src/gep/hubVerify.js +1 -1
  41. package/src/gep/issueReporter.js +87 -0
  42. package/src/gep/learningSignals.js +1 -1
  43. package/src/gep/memoryGraph.js +1 -1
  44. package/src/gep/memoryGraphAdapter.js +1 -1
  45. package/src/gep/mutation.js +1 -1
  46. package/src/gep/narrativeMemory.js +1 -1
  47. package/src/gep/openPRRegistry.js +1 -1
  48. package/src/gep/paths.js +20 -0
  49. package/src/gep/personality.js +1 -1
  50. package/src/gep/policyCheck.js +1 -1
  51. package/src/gep/prompt.js +1 -1
  52. package/src/gep/recallInject.js +1 -1
  53. package/src/gep/recallVerifier.js +1 -1
  54. package/src/gep/reflection.js +1 -1
  55. package/src/gep/sanitize.js +20 -4
  56. package/src/gep/savingsCore.js +1 -1
  57. package/src/gep/selector.js +1 -1
  58. package/src/gep/signals.js +70 -24
  59. package/src/gep/skillDistiller.js +1 -1
  60. package/src/gep/solidify.js +1 -1
  61. package/src/gep/strategy.js +1 -1
  62. package/src/gep/tokenSavings.js +1 -1
  63. package/src/gep/workspaceKeychain.js +1 -1
  64. package/src/ops/lifecycle.js +501 -31
  65. package/src/proxy/extensions/traceControl.js +1 -1
  66. package/src/proxy/index.js +4 -4
  67. package/src/proxy/inject.js +1 -1
  68. package/src/proxy/lifecycle/manager.js +233 -33
  69. package/src/proxy/sync/inbound.js +5 -4
  70. package/src/proxy/sync/outbound.js +3 -2
  71. package/src/proxy/trace/extractor.js +1 -1
  72. package/src/proxy/trace/usage.js +1 -1
package/index.js CHANGED
@@ -1,4 +1,150 @@
1
1
  #!/usr/bin/env node
2
+ function _parseBootstrapSemver(version) {
3
+ const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(version || ''));
4
+ if (!match) return null;
5
+ return {
6
+ major: match[1],
7
+ minor: match[2],
8
+ patch: match[3],
9
+ prerelease: match[4] ? match[4].split('.') : [],
10
+ };
11
+ }
12
+
13
+ function _compareBootstrapNumeric(left, right) {
14
+ if (left.length !== right.length) return left.length - right.length;
15
+ if (left < right) return -1;
16
+ if (left > right) return 1;
17
+ return 0;
18
+ }
19
+
20
+ function _compareBootstrapPrerelease(left, right) {
21
+ const leftNumeric = /^\d+$/.test(left);
22
+ const rightNumeric = /^\d+$/.test(right);
23
+ if (leftNumeric && rightNumeric) return _compareBootstrapNumeric(left, right);
24
+ if (leftNumeric) return -1;
25
+ if (rightNumeric) return 1;
26
+ if (left < right) return -1;
27
+ if (left > right) return 1;
28
+ return 0;
29
+ }
30
+
31
+ function _compareBootstrapSemver(left, right) {
32
+ const a = _parseBootstrapSemver(left);
33
+ const b = _parseBootstrapSemver(right);
34
+ if (!a || !b) return null;
35
+ for (const key of ['major', 'minor', 'patch']) {
36
+ const cmp = _compareBootstrapNumeric(a[key], b[key]);
37
+ if (cmp !== 0) return cmp;
38
+ }
39
+ if (!a.prerelease.length && !b.prerelease.length) return 0;
40
+ if (!a.prerelease.length) return 1;
41
+ if (!b.prerelease.length) return -1;
42
+ const max = Math.max(a.prerelease.length, b.prerelease.length);
43
+ for (let i = 0; i < max; i++) {
44
+ if (a.prerelease[i] === undefined) return -1;
45
+ if (b.prerelease[i] === undefined) return 1;
46
+ const cmp = _compareBootstrapPrerelease(a.prerelease[i], b.prerelease[i]);
47
+ if (cmp !== 0) return cmp;
48
+ }
49
+ return 0;
50
+ }
51
+
52
+ function _bootstrapVersionSatisfies(currentVersion, requiredVersion) {
53
+ if (String(currentVersion || '') === String(requiredVersion || '')) return true;
54
+ const cmp = _compareBootstrapSemver(currentVersion, requiredVersion);
55
+ return cmp !== null && cmp >= 0;
56
+ }
57
+
58
+ function _failClosedForceUpdateBootstrap(backupName, entryName, error) {
59
+ const detail = error && error.message ? error.message : String(error || 'unknown error');
60
+ console.error('[ForceUpdate] Bootstrap recovery failed for ' + backupName +
61
+ (entryName ? ' while restoring ' + entryName : '') + ': ' + detail);
62
+ console.error('[ForceUpdate] Refusing to continue startup; recovery backup and journal were left in place.');
63
+ process.exit(1);
64
+ }
65
+
66
+ function _recoverInterruptedForceUpdateBootstrap() {
67
+ const fs = require('fs');
68
+ const path = require('path');
69
+ const installRoot = __dirname;
70
+ const backupPrefix = '.evolver-force-update-backup-';
71
+ const journalName = '.evolver-force-update-journal.json';
72
+ let backups = [];
73
+ try {
74
+ backups = fs.readdirSync(installRoot)
75
+ .filter((name) => name.startsWith(backupPrefix))
76
+ .sort()
77
+ .reverse();
78
+ } catch (_) {
79
+ return false;
80
+ }
81
+ for (const backupName of backups) {
82
+ const backupRoot = path.join(installRoot, backupName);
83
+ const journalPath = path.join(backupRoot, journalName);
84
+ let journal = null;
85
+ try {
86
+ journal = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
87
+ } catch (_) {
88
+ // Not a force-update recovery journal; leave unrelated directories alone.
89
+ continue;
90
+ }
91
+ if (!journal || journal.state !== 'precommit' || !journal.requiredVersion) continue;
92
+ let currentVersion = '';
93
+ try {
94
+ const pkg = JSON.parse(fs.readFileSync(path.join(installRoot, 'package.json'), 'utf8'));
95
+ currentVersion = pkg && pkg.version ? String(pkg.version) : '';
96
+ } catch (_) {
97
+ // If the package marker is unreadable, prefer restoring the old payload.
98
+ }
99
+ if (_bootstrapVersionSatisfies(currentVersion, String(journal.requiredVersion))) {
100
+ try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (_) {
101
+ // Cleanup failure is non-fatal; normal startup can continue.
102
+ }
103
+ continue;
104
+ }
105
+ let entries = [];
106
+ try {
107
+ entries = fs.readdirSync(backupRoot, { withFileTypes: true });
108
+ } catch (readErr) {
109
+ _failClosedForceUpdateBootstrap(backupName, '', readErr);
110
+ }
111
+ for (const entry of entries) {
112
+ if (entry.name === journalName) continue;
113
+ const livePath = path.join(installRoot, entry.name);
114
+ const backupPath = path.join(backupRoot, entry.name);
115
+ try {
116
+ if (entry.name === 'index.js') {
117
+ const tmpPath = livePath + '.' + process.pid + '.recover-tmp';
118
+ try { fs.rmSync(tmpPath, { force: true }); } catch (_) {}
119
+ fs.copyFileSync(backupPath, tmpPath);
120
+ try {
121
+ const backupStat = fs.statSync(backupPath);
122
+ fs.chmodSync(tmpPath, backupStat.mode & 0o777);
123
+ } catch (_) {
124
+ // Recovery can proceed without mode restoration; npm/service launches
125
+ // normally use `node index.js` after an interrupted update.
126
+ }
127
+ fs.renameSync(tmpPath, livePath);
128
+ fs.rmSync(backupPath, { force: true });
129
+ } else {
130
+ fs.rmSync(livePath, { recursive: true, force: true });
131
+ fs.renameSync(backupPath, livePath);
132
+ }
133
+ } catch (restoreErr) {
134
+ _failClosedForceUpdateBootstrap(backupName, entry.name, restoreErr);
135
+ }
136
+ }
137
+ try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (_) {
138
+ // Best-effort cleanup; recovery already restored the payload.
139
+ }
140
+ console.warn('[ForceUpdate] Recovered interrupted install from ' + backupName);
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ _recoverInterruptedForceUpdateBootstrap();
147
+
2
148
  function _printProxyTokenUsage(out = process.stderr) {
3
149
  out.write('Usage: node index.js proxy-token [--settings FILE]\n');
4
150
  }
@@ -2716,8 +2862,9 @@ async function main() {
2716
2862
  // Wipe every local store of node_secret in one shot, so a daemon stuck
2717
2863
  // after a manual web reset (https://evomap.ai/account -> Reset Secret)
2718
2864
  // can boot clean. Local stores involved:
2719
- // - MailboxStore: ~/.evomap/mailbox/state.json key node_secret + version
2720
- // - Legacy files: ~/.evomap/node_secret and ~/.evomap/node_secret_version
2865
+ // - MailboxStore: ~/.evomap/mailbox/state.json node_secret state keys
2866
+ // - Legacy files: ~/.evomap/node_secret, node_secret_version,
2867
+ // node_secret_source, and node_secret_env_suppressed
2721
2868
  // - Shell env: A2A_NODE_SECRET / EVOMAP_NODE_SECRET and matching
2722
2869
  // version vars (we cannot mutate the parent shell; we
2723
2870
  // just print the unset hint)
@@ -2730,38 +2877,47 @@ async function main() {
2730
2877
  // this fallback, test/resetLocalSecret.test.js cannot inject a fake home
2731
2878
  // and the reset operates on the real user dir.
2732
2879
  const home = process.env.HOME || os.homedir();
2733
- const stateFile = path.join(home, '.evomap', 'mailbox', 'state.json');
2734
- const legacyFile = path.join(home, '.evomap', 'node_secret');
2735
- const legacyVersionFile = path.join(home, '.evomap', 'node_secret_version');
2880
+ const evomapDirs = [];
2881
+ const seenEvomapDirs = new Set();
2882
+ const addEvomapDir = (dir) => {
2883
+ if (!dir) return;
2884
+ const resolved = path.resolve(dir);
2885
+ if (seenEvomapDirs.has(resolved)) return;
2886
+ seenEvomapDirs.add(resolved);
2887
+ evomapDirs.push(dir);
2888
+ };
2889
+ addEvomapDir(path.join(home, '.evomap'));
2890
+ addEvomapDir(process.env.EVOLVER_HOME);
2736
2891
  let cleared = 0;
2737
2892
  try {
2738
- if (fs.existsSync(stateFile)) {
2739
- const raw = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
2740
- let mutated = false;
2741
- for (const k of ['node_secret', 'node_secret_source', 'node_secret_version']) {
2742
- if (raw[k] !== undefined && raw[k] !== '') {
2743
- raw[k] = '';
2744
- mutated = true;
2893
+ for (const evomapDir of evomapDirs) {
2894
+ const stateFile = path.join(evomapDir, 'mailbox', 'state.json');
2895
+ if (fs.existsSync(stateFile)) {
2896
+ const raw = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
2897
+ let mutated = false;
2898
+ for (const k of ['node_secret', 'node_secret_source', 'node_secret_version', 'node_secret_env_suppressed']) {
2899
+ if (raw[k] !== undefined && raw[k] !== '') {
2900
+ raw[k] = '';
2901
+ mutated = true;
2902
+ }
2903
+ }
2904
+ if (mutated) {
2905
+ fs.writeFileSync(stateFile, JSON.stringify(raw, null, 2) + '\n', 'utf8');
2906
+ cleared += 1;
2907
+ console.log('[reset-local-secret] cleared MailboxStore at ' + stateFile);
2908
+ } else {
2909
+ console.log('[reset-local-secret] MailboxStore had no node_secret to clear at ' + stateFile);
2745
2910
  }
2746
2911
  }
2747
- if (mutated) {
2748
- fs.writeFileSync(stateFile, JSON.stringify(raw, null, 2) + '\n', 'utf8');
2749
- cleared += 1;
2750
- console.log('[reset-local-secret] cleared MailboxStore at ' + stateFile);
2751
- } else {
2752
- console.log('[reset-local-secret] MailboxStore had no node_secret to clear');
2912
+ for (const legacyName of ['node_secret', 'node_secret_version', 'node_secret_source', 'node_secret_env_suppressed']) {
2913
+ const legacyFile = path.join(evomapDir, legacyName);
2914
+ if (fs.existsSync(legacyFile)) {
2915
+ fs.unlinkSync(legacyFile);
2916
+ cleared += 1;
2917
+ console.log('[reset-local-secret] removed legacy file ' + legacyFile);
2918
+ }
2753
2919
  }
2754
2920
  }
2755
- if (fs.existsSync(legacyFile)) {
2756
- fs.unlinkSync(legacyFile);
2757
- cleared += 1;
2758
- console.log('[reset-local-secret] removed legacy file ' + legacyFile);
2759
- }
2760
- if (fs.existsSync(legacyVersionFile)) {
2761
- fs.unlinkSync(legacyVersionFile);
2762
- cleared += 1;
2763
- console.log('[reset-local-secret] removed legacy file ' + legacyVersionFile);
2764
- }
2765
2921
  } catch (err) {
2766
2922
  console.error('[reset-local-secret] error:', err && err.message || err);
2767
2923
  process.exit(1);
@@ -2875,6 +3031,35 @@ async function main() {
2875
3031
  process.exit(1);
2876
3032
  }
2877
3033
 
3034
+ } else if (command === 'reuse') {
3035
+ try {
3036
+ const { runReuseCommand } = require('./src/gep/cliContracts');
3037
+ process.exit(await runReuseCommand(args.slice(1)));
3038
+ } catch (e) {
3039
+ process.stdout.write(JSON.stringify({
3040
+ ok: false,
3041
+ contract: 'reuse.v1',
3042
+ reason: 'internal_error',
3043
+ message: 'evolver reuse failed',
3044
+ }) + '\n');
3045
+ process.exit(1);
3046
+ }
3047
+
3048
+ } else if (command === 'publish') {
3049
+ try {
3050
+ const { runPublishCommand } = require('./src/gep/cliContracts');
3051
+ process.exit(await runPublishCommand(args.slice(1)));
3052
+ } catch (e) {
3053
+ process.stdout.write(JSON.stringify({
3054
+ ok: false,
3055
+ contract: 'publish.v1',
3056
+ reason: 'internal_error',
3057
+ retryable: false,
3058
+ message: 'evolver publish failed',
3059
+ }) + '\n');
3060
+ process.exit(1);
3061
+ }
3062
+
2878
3063
  } else if (command === 'recipe') {
2879
3064
  // recipe build — assemble a DNA blueprint from owned Gene/Capsule assets
2880
3065
  // recipe reuse — fetch + express an existing recipe into an organism
@@ -3042,7 +3227,7 @@ async function main() {
3042
3227
  }
3043
3228
 
3044
3229
  } else {
3045
- console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
3230
+ console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|reuse|publish|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
3046
3231
  - login (authorize this device via the hub, gh-auth-login style; stores an OAuth token used instead of node_secret)
3047
3232
  - logout (remove the stored OAuth token)
3048
3233
  - proxy-token (print the local proxy bearer token for command-backed client auth)
@@ -3054,6 +3239,11 @@ async function main() {
3054
3239
  - build --title="..." --genes=<asset_id,...> [--description] [--price=N] [--publish]
3055
3240
  (builds a DRAFT DNA blueprint; --publish is opt-in)
3056
3241
  - reuse --id=<recipe_id> [--input=<json>] (express a recipe into an organism)
3242
+ - reuse flags:
3243
+ - --id=<asset_id> --json (reuse a Hub asset into the local library; stdout JSON contract reuse.v1)
3244
+ - publish flags:
3245
+ - --asset=<id|path> [--asset ...] [--dry-run] --json
3246
+ (publish.v1 stdout JSON contract for Desktop)
3057
3247
  - fetch flags:
3058
3248
  - --skill=<id> | -s <id> (skill ID to download)
3059
3249
  - --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.89.14",
3
+ "version": "1.89.17",
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": {
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
3
+ const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand } = require('./hookAdapter');
4
4
 
5
5
  const HOOK_SCRIPTS_DIR_NAME = 'hooks';
6
6
  const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
@@ -146,7 +146,7 @@ function uninstall({ configRoot }) {
146
146
  const innerBefore = matcher.hooks.length;
147
147
  const filtered = matcher.hooks.filter(h => {
148
148
  const cmd = (h && h.command) || '';
149
- return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
149
+ return !isEvolverHookCommand(cmd);
150
150
  });
151
151
  // A matcher containing both evolver and user hooks shrinks
152
152
  // its inner array without changing the outer matcher count.
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
3
+ const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand } = require('./hookAdapter');
4
4
 
5
5
  const HOOK_SCRIPTS_DIR_NAME = 'hooks';
6
6
  const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
@@ -166,7 +166,7 @@ function uninstall({ configRoot }) {
166
166
  const before = data.hooks[event].length;
167
167
  data.hooks[event] = data.hooks[event].filter(h => {
168
168
  const cmd = (h && h.command) || '';
169
- return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
169
+ return !isEvolverHookCommand(cmd);
170
170
  });
171
171
  if (data.hooks[event].length !== before) touched = true;
172
172
  if (data.hooks[event].length === 0) delete data.hooks[event];
@@ -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') || c.includes('evolver-task-recall'));
79
+ return cmds.some(isEvolverHookCommand);
80
80
  };
81
81
  const userEntries = tArr.filter(e => !isEvolverOwned(e));
82
82
  result.hooks[event] = [...userEntries, ...sArr];
@@ -101,6 +101,17 @@ function collectCommands(entry) {
101
101
  return out;
102
102
  }
103
103
 
104
+ function isEvolverHookCommand(command) {
105
+ if (typeof command !== 'string') return false;
106
+ return command.includes('evolver-session') ||
107
+ command.includes('evolver-signal') ||
108
+ command.includes('evolver-task-recall') ||
109
+ // Legacy installs briefly shipped this companion daemon hook. Treat it
110
+ // as evolver-owned so reinstall/merge can remove it instead of preserving
111
+ // a stale supervisor that may point clients at a dead proxy.
112
+ command.includes('evolver-daemon-start');
113
+ }
114
+
104
115
  function deepMerge(target, source) {
105
116
  const result = { ...target };
106
117
  for (const key of Object.keys(source)) {
@@ -226,7 +237,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
226
237
  const before = data.hooks[event].length;
227
238
  data.hooks[event] = data.hooks[event].filter(h => {
228
239
  const cmd = h.command || '';
229
- return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
240
+ return !isEvolverHookCommand(cmd);
230
241
  });
231
242
  if (data.hooks[event].length !== before) changed = true;
232
243
  if (data.hooks[event].length === 0) delete data.hooks[event];
@@ -358,6 +369,7 @@ module.exports = {
358
369
  deepMerge,
359
370
  mergeWithHooksUnion,
360
371
  collectCommands,
372
+ isEvolverHookCommand,
361
373
  copyHookScripts,
362
374
  appendSectionToFile,
363
375
  assertSafeConfigDir,
@@ -14,6 +14,180 @@ const { filterRelevantOutcomes } = require('./_memoryFiltering');
14
14
  // _maybeRestartDaemon's catch-all and silently disable daemon auto-restart.
15
15
  const lockPaths = require('./_lockPaths');
16
16
 
17
+ function _readJson(file) {
18
+ try {
19
+ if (!file || !fs.existsSync(file)) return null;
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ } catch (_) {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function _isLoopbackProxyUrl(value) {
27
+ const raw = String(value || '').trim();
28
+ if (!raw) return false;
29
+ try {
30
+ const u = new URL(raw);
31
+ const host = u.hostname.toLowerCase();
32
+ return u.protocol === 'http:' && (host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]');
33
+ } catch (_) {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function _stripTomlComment(line) {
39
+ let out = '';
40
+ let quote = null;
41
+ let escaped = false;
42
+ for (const ch of String(line || '')) {
43
+ if (escaped) {
44
+ out += ch;
45
+ escaped = false;
46
+ continue;
47
+ }
48
+ if (ch === '\\' && quote === '"') {
49
+ out += ch;
50
+ escaped = true;
51
+ continue;
52
+ }
53
+ if ((ch === '"' || ch === "'") && !quote) {
54
+ quote = ch;
55
+ out += ch;
56
+ continue;
57
+ }
58
+ if (ch === quote) {
59
+ quote = null;
60
+ out += ch;
61
+ continue;
62
+ }
63
+ if (ch === '#' && !quote) break;
64
+ out += ch;
65
+ }
66
+ return out.trim();
67
+ }
68
+
69
+ function _tomlStringValue(value) {
70
+ const raw = _stripTomlComment(value);
71
+ const match = raw.match(/^(['"])([\s\S]*)\1$/);
72
+ return match ? match[2] : raw.trim();
73
+ }
74
+
75
+ function _codexConfigPath() {
76
+ if (process.env.CODEX_CONFIG_FILE || process.env.EVOMAP_CODEX_CONFIG_FILE) {
77
+ return process.env.CODEX_CONFIG_FILE || process.env.EVOMAP_CODEX_CONFIG_FILE;
78
+ }
79
+ const home = process.env.HOME || os.homedir();
80
+ return home ? path.join(home, '.codex', 'config.toml') : null;
81
+ }
82
+
83
+ function _codexConfigExpectsProxy() {
84
+ const file = _codexConfigPath();
85
+ if (!file || !fs.existsSync(file)) return false;
86
+ let selectedProvider = null;
87
+ let section = '';
88
+ const providerUrls = {};
89
+ try {
90
+ const content = fs.readFileSync(file, 'utf8');
91
+ for (const line of content.split(/\r?\n/)) {
92
+ const clean = _stripTomlComment(line);
93
+ if (!clean) continue;
94
+ const sectionMatch = clean.match(/^\[([^\]]+)\]$/);
95
+ if (sectionMatch) {
96
+ section = sectionMatch[1].trim();
97
+ continue;
98
+ }
99
+ const kv = clean.match(/^([A-Za-z0-9_.-]+)\s*=\s*([\s\S]+)$/);
100
+ if (!kv) continue;
101
+ const key = kv[1].trim();
102
+ const value = _tomlStringValue(kv[2]);
103
+ if (!section && key === 'model_provider') {
104
+ selectedProvider = value;
105
+ continue;
106
+ }
107
+ const providerMatch = section.match(/^model_providers\.([A-Za-z0-9_.-]+)$/);
108
+ if (providerMatch && key === 'base_url') {
109
+ providerUrls[providerMatch[1]] = value;
110
+ continue;
111
+ }
112
+ if (!section && key === 'base_url' && _isLoopbackProxyUrl(value)) return true;
113
+ }
114
+ } catch {
115
+ return false;
116
+ }
117
+ if (selectedProvider && _isLoopbackProxyUrl(providerUrls[selectedProvider])) return true;
118
+ return Object.keys(providerUrls).some(name =>
119
+ /(?:evomap|proxy)/i.test(name) && _isLoopbackProxyUrl(providerUrls[name])
120
+ );
121
+ }
122
+
123
+ function _proxyExpected() {
124
+ if (String(process.env.EVOMAP_PROXY || '').trim() === '1') return true;
125
+ if (String(process.env.A2A_TRANSPORT || '').trim().toLowerCase() === 'mailbox') return true;
126
+ if (_isLoopbackProxyUrl(process.env.EVOMAP_PROXY_URL) || _isLoopbackProxyUrl(process.env.ANTHROPIC_BASE_URL)) return true;
127
+ if (_codexConfigExpectsProxy()) return true;
128
+
129
+ const home = process.env.HOME || os.homedir();
130
+ const settingsFile = process.env.CLAUDE_SETTINGS_FILE || process.env.EVOMAP_CLAUDE_SETTINGS_FILE ||
131
+ (home ? path.join(home, '.claude', 'settings.json') : null);
132
+ const settings = _readJson(settingsFile);
133
+ const cfg = settings && settings.env;
134
+ return !!(cfg && (
135
+ _isLoopbackProxyUrl(cfg.EVOMAP_PROXY_URL) ||
136
+ (String(cfg.EVOMAP_PROXY_AUTO_INJECTED || '') === '1' && _isLoopbackProxyUrl(cfg.ANTHROPIC_BASE_URL))
137
+ ));
138
+ }
139
+
140
+ function _proxyReachable(url, token) {
141
+ if (!_isLoopbackProxyUrl(url) || !token) return false;
142
+ try {
143
+ const { execFileSync } = require('child_process');
144
+ execFileSync(process.execPath, ['-e', `
145
+ const fs = require('fs');
146
+ const http = require('http');
147
+ const url = process.argv[1].replace(/\\/+$/, '') + '/proxy/status';
148
+ const token = fs.readFileSync(0, 'utf8').trim();
149
+ if (!token) process.exit(1);
150
+ const req = http.get(url, { headers: { Authorization: 'Bearer ' + token } }, (res) => {
151
+ let body = '';
152
+ res.setEncoding('utf8');
153
+ res.on('data', (chunk) => {
154
+ body += chunk;
155
+ if (body.length > 1024 * 1024) req.destroy(new Error('response too large'));
156
+ });
157
+ res.on('end', () => {
158
+ if (res.statusCode < 200 || res.statusCode >= 300) process.exit(1);
159
+ let parsed;
160
+ try { parsed = JSON.parse(body); } catch (_) { process.exit(1); }
161
+ if (parsed && parsed.status === 'running' && (parsed.proxy_protocol_version || parsed.schema_version || parsed.node_id != null)) {
162
+ process.exit(0);
163
+ }
164
+ process.exit(1);
165
+ });
166
+ });
167
+ req.setTimeout(700, () => req.destroy(new Error('timeout')));
168
+ req.on('error', () => process.exit(1));
169
+ `, url], { input: String(token), stdio: ['pipe', 'ignore', 'ignore'], timeout: 1200, windowsHide: true });
170
+ return true;
171
+ } catch (_) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ function _proxyHealthyIfExpected() {
177
+ if (!_proxyExpected()) return true;
178
+ const dir = process.env.EVOLVER_SETTINGS_DIR || path.join(os.homedir(), '.evolver');
179
+ const settings = _readJson(path.join(dir, 'settings.json'));
180
+ const proxy = settings && settings.proxy;
181
+ if (!proxy || !proxy.url) return false;
182
+ if (proxy.pid) {
183
+ try { process.kill(proxy.pid, 0); } catch (e) {
184
+ if (!(e && e.code === 'EPERM')) return false;
185
+ }
186
+ }
187
+ if (!proxy.token) return false;
188
+ return _proxyReachable(proxy.url, proxy.token);
189
+ }
190
+
17
191
  // Auto-restart guard: if the evolver daemon is not running when a new agent
18
192
  // session starts, attempt a background restart. This covers the "idle-death"
19
193
  // scenario: the user closed the machine (macOS sleep), the process died due to
@@ -69,7 +243,7 @@ function _maybeRestartDaemon(evolverRoot) {
69
243
  }
70
244
  } catch (_) { /* lock file unreadable or absent: assume not running */ }
71
245
 
72
- if (daemonRunning) return; // already alive, nothing to do
246
+ if (daemonRunning && _proxyHealthyIfExpected()) return; // already alive, nothing to do
73
247
 
74
248
  // Daemon appears dead. Spawn lifecycle.js start in the background so
75
249
  // this session-start script exits immediately (< 50 ms) and does not
@@ -305,5 +479,11 @@ function main() {
305
479
  if (require.main === module) {
306
480
  main();
307
481
  } else {
308
- module.exports = { belongsToWorkspace };
482
+ module.exports = {
483
+ belongsToWorkspace,
484
+ _isLoopbackProxyUrl,
485
+ _proxyExpected,
486
+ _proxyReachable,
487
+ _proxyHealthyIfExpected,
488
+ };
309
489
  }
package/src/config.js CHANGED
@@ -85,9 +85,17 @@ const HUB_SEARCH_TIMEOUT_MS = envPositiveInt('EVOLVER_HUB_SEARCH_TIMEOUT_MS', 80
85
85
  const PUBLIC_DEFAULT_HUB_URL = 'https://evomap.ai';
86
86
 
87
87
  function resolveHubUrl() {
88
- const raw = process.env.A2A_HUB_URL
89
- || process.env.EVOMAP_HUB_URL
90
- || process.env.EVOLVER_DEFAULT_HUB_URL
88
+ // Trim each candidate and fall through on empty/whitespace-only values.
89
+ // A trailing space in a `.env` value (common with quoted entries like
90
+ // A2A_HUB_URL="https://evomap.ai ") otherwise survives into endpoint
91
+ // construction: `hubUrl.replace(/\/+$/, '')` strips trailing slashes but
92
+ // NOT whitespace, producing "https://evomap.ai /a2a/events/poll" which
93
+ // fails URL validation (#580 Bug 1). `||` alone would also pick a
94
+ // whitespace-only override as "truthy", so trim-then-fall-through here.
95
+ const pick = (v) => { const t = (v == null ? '' : String(v)).trim(); return t || null; };
96
+ const raw = pick(process.env.A2A_HUB_URL)
97
+ || pick(process.env.EVOMAP_HUB_URL)
98
+ || pick(process.env.EVOLVER_DEFAULT_HUB_URL)
91
99
  || PUBLIC_DEFAULT_HUB_URL;
92
100
 
93
101
  if (process.env.EVOMAP_HUB_ALLOW_INSECURE !== '1') {