@evomap/evolver-mcp 2.0.0-beta.8 → 2.0.0

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.
@@ -1,14 +1,17 @@
1
1
  import { lstatSync, readFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
  export class EvolverProxyClient {
5
6
  baseUrl;
6
7
  token;
7
8
  fetchFn;
9
+ expectedHubMode;
8
10
  reloadSettings;
9
11
  constructor(opts) {
10
12
  this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
11
13
  this.token = opts.token;
14
+ this.expectedHubMode = opts.expectedHubMode;
12
15
  this.fetchFn = opts.fetchFn ?? globalFetch;
13
16
  this.reloadSettings = opts.reloadSettings;
14
17
  }
@@ -16,6 +19,7 @@ export class EvolverProxyClient {
16
19
  return this.call('GET', '/proxy/status', undefined, opts);
17
20
  }
18
21
  search(args) {
22
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
19
23
  return this.call('POST', '/asset/search', {
20
24
  ...(args.text ? { text: args.text } : {}),
21
25
  ...(args.signalsAny && args.signalsAny.length > 0 ? { signals: args.signalsAny } : {}),
@@ -23,12 +27,15 @@ export class EvolverProxyClient {
23
27
  ...(args.category ? { category: args.category } : {}),
24
28
  ...(args.gene ? { gene: args.gene } : {}),
25
29
  ...(args.limit !== undefined ? { limit: args.limit } : {}),
30
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
26
31
  });
27
32
  }
28
33
  fetchAsset(args) {
34
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
29
35
  return this.call('POST', '/asset/fetch', {
30
36
  ...(args.assetId ? { asset_id: args.assetId } : {}),
31
37
  ...(args.assetIds ? { asset_ids: args.assetIds } : {}),
38
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
32
39
  });
33
40
  }
34
41
  searchAgents(args) {
@@ -45,19 +52,27 @@ export class EvolverProxyClient {
45
52
  });
46
53
  }
47
54
  submitAsset(asset) {
48
- return this.call('POST', '/asset/submit', { assets: [asset] });
55
+ // MCP publishing remains durable and outage-tolerant; the bare route is reserved for V1 synchronous callers.
56
+ return this.call('POST', '/asset/submit?mode=async', this.modeBoundBody({
57
+ assets: [asset],
58
+ request_id: randomUUID(),
59
+ }));
60
+ }
61
+ submitAssetBundle(bundle) {
62
+ return this.call('POST', '/asset/submit', this.modeBoundBody(bundle));
49
63
  }
50
64
  /** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
51
65
  validateAsset(asset) {
52
66
  return this.validateAssetBundle({ assets: [asset] });
53
67
  }
54
68
  validateAssetBundle(bundle) {
55
- return this.call('POST', '/asset/validate', bundle);
69
+ return this.call('POST', '/asset/validate', this.modeBoundBody(bundle));
56
70
  }
57
71
  distillConversation(input) {
58
- return this.call('POST', '/conversation/distill', input);
72
+ return this.call('POST', '/conversation/distill', this.modeBoundBody(input));
59
73
  }
60
74
  recordReuseResult(args) {
75
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
61
76
  return this.call('POST', '/asset/reuse-result', {
62
77
  asset_id: args.assetId,
63
78
  outcome: args.outcome,
@@ -65,43 +80,84 @@ export class EvolverProxyClient {
65
80
  ...(args.traceId ? { trace_id: args.traceId } : {}),
66
81
  ...(args.timeSavedSeconds !== undefined ? { time_saved_seconds: args.timeSavedSeconds } : {}),
67
82
  ...(args.reason ? { reason: args.reason } : {}),
83
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
68
84
  });
69
85
  }
86
+ modeBoundBody(input) {
87
+ if (!this.expectedHubMode || !input || typeof input !== 'object' || Array.isArray(input))
88
+ return input;
89
+ const body = input;
90
+ return { ...body, expected_hub_mode: body['expected_hub_mode'] ?? this.expectedHubMode };
91
+ }
70
92
  async call(method, path, body, opts = {}) {
93
+ let connection = this.connectionSnapshot();
71
94
  try {
72
- const result = await this.callOnce(method, path, body, opts);
95
+ if (path !== '/proxy/status' && this.expectedHubMode === 'private') {
96
+ await this.verifyExpectedHubMode(connection, opts);
97
+ }
98
+ const result = await this.callOnce(method, path, body, opts, connection);
73
99
  if (result.ok)
74
- return result.parsed;
100
+ return this.acceptResult(result, path);
75
101
  if (result.status === 401 && this.reloadFromSettings()) {
76
- const retry = await this.callOnce(method, path, body, opts);
102
+ connection = this.connectionSnapshot();
103
+ await this.verifyReloadedHubMode(path, connection, opts);
104
+ const retry = await this.callOnce(method, path, body, opts, connection);
77
105
  if (retry.ok)
78
- return retry.parsed;
106
+ return this.acceptResult(retry, path);
79
107
  throw this.proxyError(retry, path);
80
108
  }
81
109
  throw this.proxyError(result, path);
82
110
  }
83
111
  catch (err) {
84
112
  if (this.reloadFromSettings()) {
85
- const retry = await this.callOnce(method, path, body, opts);
113
+ connection = this.connectionSnapshot();
114
+ await this.verifyReloadedHubMode(path, connection, opts);
115
+ const retry = await this.callOnce(method, path, body, opts, connection);
86
116
  if (retry.ok)
87
- return retry.parsed;
117
+ return this.acceptResult(retry, path);
88
118
  throw this.proxyError(retry, path);
89
119
  }
90
120
  throw err;
91
121
  }
92
122
  }
93
- async callOnce(method, path, body, opts) {
94
- const res = await this.fetchFn(`${this.baseUrl}${path}`, {
123
+ async verifyExpectedHubMode(connection, opts) {
124
+ // A proxy can restart on the same loopback URL with the same operator-supplied token. Verify every private
125
+ // operation against the same immutable connection snapshot used for its payload. This prevents a concurrent
126
+ // settings reload from moving the payload to an endpoint that the status probe never verified.
127
+ const result = await this.callOnce('GET', '/proxy/status', undefined, opts, connection);
128
+ if (!result.ok)
129
+ throw this.proxyError(result, '/proxy/status');
130
+ this.acceptResult(result, '/proxy/status');
131
+ }
132
+ async verifyReloadedHubMode(path, connection, opts) {
133
+ if (path !== '/proxy/status' && this.expectedHubMode === 'private') {
134
+ await this.verifyExpectedHubMode(connection, opts);
135
+ }
136
+ }
137
+ acceptResult(result, path) {
138
+ if (path === '/proxy/status' && this.expectedHubMode === 'private') {
139
+ const status = recordValue(result.parsed);
140
+ if (status['hub_mode'] !== 'private')
141
+ throw new Error('proxy_hub_mode_mismatch');
142
+ }
143
+ return result.parsed;
144
+ }
145
+ async callOnce(method, path, body, opts, connection) {
146
+ const res = await this.fetchFn(`${connection.baseUrl}${path}`, {
95
147
  method,
96
148
  headers: {
97
- authorization: `Bearer ${this.token}`,
149
+ authorization: `Bearer ${connection.token}`,
98
150
  'content-type': 'application/json',
151
+ ...(this.expectedHubMode ? { 'x-evomap-expected-hub-mode': this.expectedHubMode } : {}),
99
152
  },
100
153
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
101
154
  ...(opts.signal ? { signal: opts.signal } : {}),
102
155
  });
103
156
  return { ok: res.ok, status: res.status, parsed: await res.json() };
104
157
  }
158
+ connectionSnapshot() {
159
+ return { baseUrl: this.baseUrl, token: this.token };
160
+ }
105
161
  reloadFromSettings() {
106
162
  const next = this.reloadSettings?.();
107
163
  if (!next)
@@ -133,38 +189,46 @@ function agentDirectoryBody(args) {
133
189
  };
134
190
  }
135
191
  export function proxyClientFromEnv(env = process.env) {
192
+ const expectedHubMode = expectedHubModeFromEnv(env);
193
+ if (!expectedHubMode)
194
+ return undefined;
136
195
  const token = env['EVOLVER_IPC_TOKEN']?.trim();
137
196
  if (!token)
138
- return proxyClientFromSettings(env, env === process.env);
139
- const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
140
- const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
141
- return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token });
197
+ return proxyClientFromSettings(env, env === process.env, undefined, expectedHubMode);
198
+ const baseUrl = proxyBaseUrlFromEnv(env);
199
+ return baseUrl ? new EvolverProxyClient({ baseUrl, token, expectedHubMode }) : undefined;
142
200
  }
143
201
  export async function reachableProxyClientFromEnv(env = process.env, opts = {}) {
202
+ const expectedHubMode = expectedHubModeFromEnv(env);
203
+ if (!expectedHubMode)
204
+ return undefined;
144
205
  const token = env['EVOLVER_IPC_TOKEN']?.trim();
145
206
  if (token) {
146
- const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
147
- const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
148
- return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token, ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}) });
207
+ const baseUrl = proxyBaseUrlFromEnv(env);
208
+ return baseUrl ? new EvolverProxyClient({ baseUrl, token, expectedHubMode, ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}) }) : undefined;
149
209
  }
150
- const client = proxyClientFromSettings(env, env === process.env, opts.fetchFn);
210
+ const client = proxyClientFromSettings(env, env === process.env, opts.fetchFn, expectedHubMode);
151
211
  if (!client)
152
212
  return undefined;
153
213
  return await proxyClientReachable(client, opts.timeoutMs ?? 250) ? client : undefined;
154
214
  }
155
- function proxyClientFromSettings(env, allowDefaultHome, fetchFn) {
215
+ function proxyClientFromSettings(env, allowDefaultHome, fetchFn, expectedHubMode = 'public') {
156
216
  const settings = readProxySettings(env, allowDefaultHome);
157
217
  return settings ? new EvolverProxyClient({
158
218
  ...settings,
219
+ expectedHubMode,
159
220
  ...(fetchFn ? { fetchFn } : {}),
160
221
  reloadSettings: () => readProxySettings(env, allowDefaultHome),
161
222
  }) : undefined;
162
223
  }
224
+ function expectedHubModeFromEnv(env) {
225
+ const value = env['EVOMAP_HUB_MODE']?.trim().toLowerCase() || 'public';
226
+ return value === 'public' || value === 'private' ? value : undefined;
227
+ }
163
228
  function readProxySettings(env, allowDefaultHome) {
164
- const homeDir = env['HOME']?.trim() || (allowDefaultHome ? homedir() : '');
165
- if (!homeDir)
229
+ const settingsPath = resolveProxySettingsPath(env, allowDefaultHome);
230
+ if (!settingsPath)
166
231
  return undefined;
167
- const settingsPath = join(homeDir, '.evolver', 'settings.json');
168
232
  try {
169
233
  if (!lstatSync(settingsPath).isFile())
170
234
  return undefined;
@@ -180,11 +244,23 @@ function readProxySettings(env, allowDefaultHome) {
180
244
  return undefined;
181
245
  }
182
246
  }
247
+ function resolveProxySettingsPath(env, allowDefaultHome) {
248
+ const explicit = env['EVOLVER_PROXY_SETTINGS_FILE']?.trim();
249
+ if (explicit)
250
+ return explicit;
251
+ const settingsDir = env['EVOLVER_SETTINGS_DIR']?.trim();
252
+ if (settingsDir)
253
+ return join(settingsDir, 'settings.json');
254
+ const homeDir = env['HOME']?.trim() || (allowDefaultHome ? homedir() : '');
255
+ return homeDir ? join(homeDir, '.evolver', 'settings.json') : undefined;
256
+ }
183
257
  function isLoopbackHttpUrl(raw) {
184
258
  try {
185
259
  const url = new URL(raw);
186
260
  if (url.protocol !== 'http:' && url.protocol !== 'https:')
187
261
  return false;
262
+ if (url.username || url.password || url.search || url.hash || (url.pathname && url.pathname !== '/'))
263
+ return false;
188
264
  const hostname = url.hostname.toLowerCase();
189
265
  return hostname === '127.0.0.1'
190
266
  || hostname === 'localhost'
@@ -195,6 +271,19 @@ function isLoopbackHttpUrl(raw) {
195
271
  return false;
196
272
  }
197
273
  }
274
+ function proxyBaseUrlFromEnv(env) {
275
+ const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
276
+ if (explicitUrl)
277
+ return isLoopbackHttpUrl(explicitUrl) ? explicitUrl : undefined;
278
+ const rawPort = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
279
+ if (!/^\d+$/.test(rawPort))
280
+ return undefined;
281
+ const port = Number(rawPort);
282
+ if (!Number.isInteger(port) || port < 0 || port > 65_535)
283
+ return undefined;
284
+ const baseUrl = `http://127.0.0.1:${port}`;
285
+ return isLoopbackHttpUrl(baseUrl) ? baseUrl : undefined;
286
+ }
198
287
  function recordValue(value) {
199
288
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
200
289
  }
@@ -205,7 +294,9 @@ async function proxyClientReachable(client, timeoutMs) {
205
294
  await client.status({ signal: controller.signal });
206
295
  return true;
207
296
  }
208
- catch {
297
+ catch (error) {
298
+ if (error instanceof Error && error.message === 'proxy_hub_mode_mismatch')
299
+ throw error;
209
300
  return false;
210
301
  }
211
302
  finally {
@@ -214,5 +305,5 @@ async function proxyClientReachable(client, timeoutMs) {
214
305
  }
215
306
  }
216
307
  async function globalFetch(url, init) {
217
- return fetch(url, init);
308
+ return fetch(url, { ...init, redirect: 'error' });
218
309
  }
@@ -0,0 +1,20 @@
1
+ import { linkSync } from 'node:fs';
2
+ export interface SharedFileCommitOptions {
3
+ path: string;
4
+ expectedRaw: string | undefined;
5
+ nextRaw: string | undefined;
6
+ mode?: number;
7
+ beforeCommitForTest?: () => void;
8
+ afterValidateForTest?: () => void;
9
+ afterDisplaceForTest?: (displacedPath: string) => void;
10
+ beforePublishForTest?: () => void;
11
+ linkForTest?: typeof linkSync;
12
+ }
13
+ export declare class SharedFileConflictError extends Error {
14
+ readonly recoveryPath?: string | undefined;
15
+ constructor(path: string, recoveryPath?: string | undefined, options?: {
16
+ cause?: unknown;
17
+ });
18
+ }
19
+ /** Commits only if the target bytes still match the caller's snapshot. */
20
+ export declare function commitSharedFile(options: SharedFileCommitOptions): void;
@@ -0,0 +1,256 @@
1
+ import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, linkSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ export class SharedFileConflictError extends Error {
5
+ recoveryPath;
6
+ constructor(path, recoveryPath, options) {
7
+ super(recoveryPath
8
+ ? `Shared config changed during commit: ${path}; conflicting bytes preserved at ${recoveryPath}`
9
+ : `Shared config changed during commit: ${path}`, options?.cause === undefined ? undefined : { cause: options.cause });
10
+ this.recoveryPath = recoveryPath;
11
+ this.name = 'SharedFileConflictError';
12
+ }
13
+ }
14
+ function tempPath(path, label) {
15
+ return join(dirname(path), `.${basename(path)}.evolver-${label}-${process.pid}-${randomUUID()}`);
16
+ }
17
+ function writePrepared(path, raw, mode) {
18
+ const fd = openSync(path, 'wx', mode);
19
+ try {
20
+ writeFileSync(fd, raw, 'utf8');
21
+ }
22
+ finally {
23
+ closeSync(fd);
24
+ }
25
+ chmodSync(path, mode);
26
+ }
27
+ function removeIfPresent(path) {
28
+ if (!path)
29
+ return;
30
+ try {
31
+ unlinkSync(path);
32
+ }
33
+ catch (err) {
34
+ if (err.code !== 'ENOENT')
35
+ throw err;
36
+ }
37
+ }
38
+ function publishNoClobber(source, target, linkFile = linkSync) {
39
+ try {
40
+ linkFile(source, target);
41
+ }
42
+ catch (error) {
43
+ const code = error.code;
44
+ if (code !== 'EPERM' && code !== 'EACCES' && code !== 'ENOTSUP' && code !== 'EOPNOTSUPP' && code !== 'EXDEV') {
45
+ throw error;
46
+ }
47
+ copyFileSync(source, target, fsConstants.COPYFILE_EXCL);
48
+ }
49
+ }
50
+ function snapshotNoClobber(source, snapshot, linkFile = linkSync) {
51
+ try {
52
+ linkFile(source, snapshot);
53
+ }
54
+ catch (error) {
55
+ const code = error.code;
56
+ if (code !== 'EPERM' && code !== 'EACCES' && code !== 'ENOTSUP' && code !== 'EOPNOTSUPP' && code !== 'EXDEV') {
57
+ throw error;
58
+ }
59
+ copyFileSync(source, snapshot, fsConstants.COPYFILE_EXCL);
60
+ }
61
+ }
62
+ function restoreNoClobber(displaced, target, linkFile = linkSync) {
63
+ try {
64
+ publishNoClobber(displaced, target, linkFile);
65
+ unlinkSync(displaced);
66
+ return undefined;
67
+ }
68
+ catch (err) {
69
+ const code = err.code;
70
+ if (code === 'EEXIST')
71
+ return displaced;
72
+ if (code === 'EPERM' || code === 'EACCES' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EXDEV') {
73
+ try {
74
+ copyFileSync(displaced, target, fsConstants.COPYFILE_EXCL);
75
+ unlinkSync(displaced);
76
+ return undefined;
77
+ }
78
+ catch (copyError) {
79
+ if (copyError.code === 'EEXIST')
80
+ return displaced;
81
+ throw new AggregateError([err, copyError], `Unable to restore shared config at ${target}`);
82
+ }
83
+ }
84
+ throw err;
85
+ }
86
+ }
87
+ function fileVersion(path) {
88
+ const stat = statSync(path, { bigint: true });
89
+ return {
90
+ dev: stat.dev,
91
+ ino: stat.ino,
92
+ size: stat.size,
93
+ mtimeNs: stat.mtimeNs,
94
+ ctimeNs: stat.ctimeNs,
95
+ };
96
+ }
97
+ function sameFileVersion(left, right) {
98
+ return left.dev === right.dev
99
+ && left.ino === right.ino
100
+ && left.size === right.size
101
+ && left.mtimeNs === right.mtimeNs
102
+ && left.ctimeNs === right.ctimeNs;
103
+ }
104
+ /** Commits only if the target bytes still match the caller's snapshot. */
105
+ export function commitSharedFile(options) {
106
+ const mode = options.mode ?? 0o600;
107
+ const prepared = options.nextRaw === undefined ? undefined : tempPath(options.path, 'next');
108
+ const displaced = options.expectedRaw === undefined ? undefined : tempPath(options.path, 'previous');
109
+ const movedLive = options.expectedRaw === undefined ? undefined : tempPath(options.path, 'live');
110
+ let preserveDisplaced = false;
111
+ let preserveMovedLive = false;
112
+ let liveWasMoved = false;
113
+ let published = false;
114
+ try {
115
+ if (prepared)
116
+ writePrepared(prepared, options.nextRaw, mode);
117
+ options.beforeCommitForTest?.();
118
+ if (options.expectedRaw === undefined) {
119
+ if (!prepared)
120
+ return;
121
+ try {
122
+ publishNoClobber(prepared, options.path, options.linkForTest);
123
+ }
124
+ catch (err) {
125
+ if (err.code === 'EEXIST') {
126
+ throw new SharedFileConflictError(options.path);
127
+ }
128
+ throw err;
129
+ }
130
+ return;
131
+ }
132
+ // Validate while the live path is still present. The no-clobber publish below then limits the unavoidable
133
+ // crash-only missing-path interval to the two adjacent metadata operations.
134
+ let versionBeforeRead;
135
+ let actualRaw;
136
+ let validatedVersion;
137
+ try {
138
+ versionBeforeRead = fileVersion(options.path);
139
+ actualRaw = readFileSync(options.path, 'utf8');
140
+ validatedVersion = fileVersion(options.path);
141
+ }
142
+ catch (err) {
143
+ if (err.code === 'ENOENT') {
144
+ throw new SharedFileConflictError(options.path);
145
+ }
146
+ throw err;
147
+ }
148
+ if (actualRaw !== options.expectedRaw) {
149
+ throw new SharedFileConflictError(options.path);
150
+ }
151
+ if (!sameFileVersion(versionBeforeRead, validatedVersion)) {
152
+ throw new SharedFileConflictError(options.path);
153
+ }
154
+ options.afterValidateForTest?.();
155
+ snapshotNoClobber(options.path, displaced, options.linkForTest);
156
+ try {
157
+ const liveAfterSnapshot = fileVersion(options.path);
158
+ if (readFileSync(options.path, 'utf8') !== options.expectedRaw
159
+ || liveAfterSnapshot.dev !== validatedVersion.dev
160
+ || liveAfterSnapshot.ino !== validatedVersion.ino
161
+ || liveAfterSnapshot.size !== validatedVersion.size
162
+ || liveAfterSnapshot.mtimeNs !== validatedVersion.mtimeNs) {
163
+ throw new SharedFileConflictError(options.path);
164
+ }
165
+ options.afterDisplaceForTest?.(displaced);
166
+ const liveBeforePublish = fileVersion(options.path);
167
+ if (!sameFileVersion(liveAfterSnapshot, liveBeforePublish)
168
+ || readFileSync(options.path, 'utf8') !== options.expectedRaw
169
+ || readFileSync(displaced, 'utf8') !== options.expectedRaw) {
170
+ throw new SharedFileConflictError(options.path);
171
+ }
172
+ options.beforePublishForTest?.();
173
+ renameSync(options.path, movedLive);
174
+ liveWasMoved = true;
175
+ if (readFileSync(movedLive, 'utf8') !== options.expectedRaw) {
176
+ const recoveryPath = restoreNoClobber(movedLive, options.path, options.linkForTest);
177
+ liveWasMoved = recoveryPath !== undefined;
178
+ preserveMovedLive = recoveryPath !== undefined;
179
+ throw new SharedFileConflictError(options.path, recoveryPath);
180
+ }
181
+ if (prepared) {
182
+ publishNoClobber(prepared, options.path);
183
+ removeIfPresent(prepared);
184
+ }
185
+ else {
186
+ unlinkSync(movedLive);
187
+ liveWasMoved = false;
188
+ }
189
+ if (prepared) {
190
+ unlinkSync(movedLive);
191
+ liveWasMoved = false;
192
+ }
193
+ published = true;
194
+ removeIfPresent(displaced);
195
+ }
196
+ catch (error) {
197
+ if (!published) {
198
+ if (liveWasMoved && movedLive !== undefined) {
199
+ const recoveryPath = restoreNoClobber(movedLive, options.path, options.linkForTest);
200
+ liveWasMoved = recoveryPath !== undefined;
201
+ preserveMovedLive = recoveryPath !== undefined;
202
+ if (recoveryPath !== undefined) {
203
+ removeIfPresent(displaced);
204
+ throw new SharedFileConflictError(options.path, recoveryPath, { cause: error });
205
+ }
206
+ }
207
+ removeIfPresent(displaced);
208
+ if (error instanceof SharedFileConflictError)
209
+ throw error;
210
+ throw new SharedFileConflictError(options.path, undefined, { cause: error });
211
+ }
212
+ let recoveryPath;
213
+ try {
214
+ if (prepared && existsSync(options.path)) {
215
+ try {
216
+ if (readFileSync(options.path, 'utf8') === options.nextRaw)
217
+ removeIfPresent(options.path);
218
+ }
219
+ catch {
220
+ // Preserve a concurrent replacement and expose the snapshot as recoveryPath.
221
+ }
222
+ }
223
+ recoveryPath = restoreNoClobber(displaced, options.path, options.linkForTest);
224
+ preserveDisplaced = recoveryPath !== undefined;
225
+ }
226
+ catch (restoreError) {
227
+ preserveDisplaced = true;
228
+ throw new Error(`Shared config commit failed for ${options.path}; original bytes preserved at ${displaced}`, { cause: new AggregateError([error, restoreError]) });
229
+ }
230
+ throw new SharedFileConflictError(options.path, recoveryPath, { cause: error });
231
+ }
232
+ }
233
+ finally {
234
+ if (!preserveDisplaced && displaced && existsSync(displaced)) {
235
+ if (!existsSync(options.path)) {
236
+ const recoveryPath = restoreNoClobber(displaced, options.path, options.linkForTest);
237
+ preserveDisplaced = recoveryPath !== undefined;
238
+ }
239
+ else {
240
+ // Keep a displaced file only when it is the sole recovery copy of concurrent bytes.
241
+ try {
242
+ const displacedRaw = readFileSync(displaced, 'utf8');
243
+ if (displacedRaw === options.expectedRaw)
244
+ removeIfPresent(displaced);
245
+ }
246
+ catch {
247
+ // Preserve an unreadable displaced file instead of masking the primary result.
248
+ }
249
+ }
250
+ }
251
+ removeIfPresent(prepared);
252
+ if (!preserveMovedLive && movedLive && existsSync(movedLive)) {
253
+ removeIfPresent(movedLive);
254
+ }
255
+ }
256
+ }
package/dist/stdio.js CHANGED
@@ -8,11 +8,17 @@ import { buildEvolverTools } from './tools.js';
8
8
  import { buildEvolverPrimer } from './primer.js';
9
9
  import { EvolverMcpServer, UnknownToolError } from './server.js';
10
10
  import { reachableProxyClientFromEnv } from './proxyClient.js';
11
- import { loadEnvFileFromEnv } from './envFile.js';
12
- const envFile = loadEnvFileFromEnv(process.env);
13
- if (envFile.error) {
14
- process.stderr.write(`[evolver-mcp] failed to load EVOLVER_ENV_FILE: ${envFile.error}\n`);
11
+ import { loadEnvFileFromEnvOrThrow } from './envFile.js';
12
+ import { bootstrap } from '@evomap/evolver-core';
13
+ try {
14
+ loadEnvFileFromEnvOrThrow(process.env);
15
15
  }
16
+ catch {
17
+ process.stderr.write('[evolver-mcp] fatal: failed to load EVOLVER_ENV_FILE\n');
18
+ process.exit(1);
19
+ }
20
+ // Emit deprecation warnings for any V1 env vars still present in the environment.
21
+ bootstrap.checkV1EnvCompat(process.env);
16
22
  const store = new assetstore.LocalJsonlProvider(events.assetsDir());
17
23
  const mailboxPath = process.env['EVOLVER_MCP_MAILBOX'] ?? join(events.evomapHome(), 'mailbox', 'mcp.db');
18
24
  mkdirSync(dirname(mailboxPath), { recursive: true });
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@evomap/evolver-mcp",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": "^22.13.0 || >=23.4.0"
8
+ },
6
9
  "description": "Evolver MCP server (agent 工具发现入口)",
7
10
  "bin": {
8
11
  "evolver-mcp": "./dist/stdio.js"
@@ -20,7 +23,7 @@
20
23
  }
21
24
  },
22
25
  "dependencies": {
23
- "@evomap/evolver-core": "2.0.0-beta.8",
26
+ "@evomap/evolver-core": "2.0.0",
24
27
  "smol-toml": "^1.6.1"
25
28
  },
26
29
  "repository": {
@@ -29,7 +32,7 @@
29
32
  },
30
33
  "publishConfig": {
31
34
  "access": "public",
32
- "tag": "v2-beta"
35
+ "tag": "latest"
33
36
  },
34
37
  "files": [
35
38
  "dist/",