@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.
- package/dist/antigravityInstaller.d.ts +1 -1
- package/dist/antigravityInstaller.js +17 -29
- package/dist/codexInstaller.d.ts +8 -3
- package/dist/codexInstaller.js +203 -50
- package/dist/cursorRulesInstaller.d.ts +1 -1
- package/dist/cursorRulesInstaller.js +66 -17
- package/dist/envFile.d.ts +2 -1
- package/dist/envFile.js +6 -0
- package/dist/installer.d.ts +15 -24
- package/dist/installer.js +224 -142
- package/dist/installerShared.d.ts +103 -0
- package/dist/installerShared.js +99 -0
- package/dist/jsonMcpInstaller.d.ts +5 -1
- package/dist/jsonMcpInstaller.js +54 -21
- package/dist/kiroInstaller.d.ts +3 -3
- package/dist/opencodeInstaller.d.ts +3 -3
- package/dist/proxyClient.d.ts +12 -0
- package/dist/proxyClient.js +117 -26
- package/dist/sharedFileCommit.d.ts +20 -0
- package/dist/sharedFileCommit.js +256 -0
- package/dist/stdio.js +10 -4
- package/package.json +6 -3
package/dist/proxyClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
+
return this.acceptResult(result, path);
|
|
75
101
|
if (result.status === 401 && this.reloadFromSettings()) {
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
94
|
-
|
|
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 ${
|
|
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
|
|
140
|
-
|
|
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
|
|
147
|
-
|
|
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
|
|
165
|
-
if (!
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
process.
|
|
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
|
|
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
|
|
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": "
|
|
35
|
+
"tag": "latest"
|
|
33
36
|
},
|
|
34
37
|
"files": [
|
|
35
38
|
"dist/",
|