@evomap/evolver-mcp 2.0.0-beta.17 → 2.0.0-beta.19
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/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.js +17 -7
- package/dist/sharedFileCommit.d.ts +20 -0
- package/dist/sharedFileCommit.js +256 -0
- package/dist/stdio.js +3 -0
- package/package.json +5 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
/** Default command used by runtimes that support a SessionStart hook. The flag enables session-id capture. */
|
|
4
|
+
export const DEFAULT_HOOK_COMMAND = 'evolver inject session-start --hook-stdin';
|
|
5
|
+
export const DEFAULT_PROMPT_RECALL_HOOK_COMMAND = 'evolver inject prompt-recall --hook-stdin';
|
|
6
|
+
export const EVOLVER_HOOK_STATUS = 'evolver-managed-hook';
|
|
7
|
+
const LEGACY_EVOLVER_HOOK_STATUS = 'Loading Evolver memory';
|
|
8
|
+
function isObj(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
const LEGACY_NODE_COMMAND = /^"?node(?:\.exe)?"?\s+/i;
|
|
12
|
+
const LEGACY_EVOLVER_SCRIPT_BASENAME = /(?:^|[\\/'"\s])(?:evolver-session-start|evolver-session-end|evolver-signal-detect|evolver-task-recall|evolver-daemon-start)\.js(?=$|[\\/'"\s])/i;
|
|
13
|
+
const LEGACY_V2_CLI_HOOK_COMMAND = /^"?evolver(?:\.cmd|\.exe)?"?\s+inject\s+(?:session-start|prompt-recall)(?:\s|$)/i;
|
|
14
|
+
function isKnownEvolverHookCommand(command) {
|
|
15
|
+
const trimmed = command.trim();
|
|
16
|
+
return LEGACY_V2_CLI_HOOK_COMMAND.test(trimmed)
|
|
17
|
+
|| (LEGACY_NODE_COMMAND.test(trimmed) && LEGACY_EVOLVER_SCRIPT_BASENAME.test(trimmed));
|
|
18
|
+
}
|
|
19
|
+
/** Bind custom-command ownership to that exact command without adding undocumented hook-schema fields. */
|
|
20
|
+
export function evolverManagedHookStatus(command) {
|
|
21
|
+
if (isKnownEvolverHookCommand(command))
|
|
22
|
+
return EVOLVER_HOOK_STATUS;
|
|
23
|
+
const commandTag = createHash('sha256').update(command, 'utf8').digest('hex').slice(0, 16);
|
|
24
|
+
return `${EVOLVER_HOOK_STATUS} [evolver:${commandTag}]`;
|
|
25
|
+
}
|
|
26
|
+
function legacyEvolverManagedHookStatus(command) {
|
|
27
|
+
const currentStatus = evolverManagedHookStatus(command);
|
|
28
|
+
return `${LEGACY_EVOLVER_HOOK_STATUS}${currentStatus.slice(EVOLVER_HOOK_STATUS.length)}`;
|
|
29
|
+
}
|
|
30
|
+
const NO_MANAGED_HOOK_COMMANDS = new Set();
|
|
31
|
+
const isEvolverHandler = (handler, trustManagedStatus, managedCommands) => {
|
|
32
|
+
if (!isObj(handler) || typeof handler['command'] !== 'string')
|
|
33
|
+
return false;
|
|
34
|
+
const command = handler['command'];
|
|
35
|
+
return isKnownEvolverHookCommand(command)
|
|
36
|
+
|| (trustManagedStatus && (managedCommands.has(command)
|
|
37
|
+
|| handler['statusMessage'] === evolverManagedHookStatus(command)
|
|
38
|
+
|| handler['statusMessage'] === legacyEvolverManagedHookStatus(command)));
|
|
39
|
+
};
|
|
40
|
+
/** Remove only Evolver-owned handlers, retaining user handlers that share the same matcher group. */
|
|
41
|
+
export function stripEvolverHookEntries(entries, trustManagedStatus = false, managedCommands = NO_MANAGED_HOOK_COMMANDS) {
|
|
42
|
+
let changed = false;
|
|
43
|
+
const keptEntries = [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!isObj(entry)) {
|
|
46
|
+
keptEntries.push(entry);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const handlers = entry['hooks'];
|
|
50
|
+
if (!Array.isArray(handlers)) {
|
|
51
|
+
if (isEvolverHandler(entry, trustManagedStatus, managedCommands))
|
|
52
|
+
changed = true;
|
|
53
|
+
else
|
|
54
|
+
keptEntries.push(entry);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const keptHandlers = handlers.filter((handler) => !isEvolverHandler(handler, trustManagedStatus, managedCommands));
|
|
58
|
+
if (keptHandlers.length === handlers.length) {
|
|
59
|
+
keptEntries.push(entry);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
changed = true;
|
|
63
|
+
if (keptHandlers.length > 0)
|
|
64
|
+
keptEntries.push({ ...entry, hooks: keptHandlers });
|
|
65
|
+
}
|
|
66
|
+
return { changed, entries: keptEntries };
|
|
67
|
+
}
|
|
68
|
+
export class SymlinkRefusedError extends Error {
|
|
69
|
+
constructor(label, path) {
|
|
70
|
+
super(`[setup-hooks] refusing to operate: ${label} ${path} is a symbolic link — evolver will not follow symlinks for adapter-owned paths (a hostile workspace could redirect writes/unlinks outside the project). Replace it with a real directory/file and rerun.`);
|
|
71
|
+
this.name = 'SymlinkRefusedError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class UnparseableConfigError extends Error {
|
|
75
|
+
constructor(label, path, owner = 'Claude Code') {
|
|
76
|
+
super(`[setup-hooks] refusing to overwrite ${label} (${path}): the file exists and is non-empty but is not valid JSON. This is ${owner}'s own shared config; merging into it would replace the whole file and could wipe its contents. Fix or remove the corrupt file, then rerun.`);
|
|
77
|
+
this.name = 'UnparseableConfigError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class EmptySharedConfigError extends Error {
|
|
81
|
+
constructor(label, path, owner = 'Claude Code') {
|
|
82
|
+
super(`[setup-hooks] refusing to overwrite ${label} (${path}): the file exists but is empty or contains only whitespace. ${owner} may be in the middle of a truncating write, and treating it as fresh config could wipe shared config data. Fix the empty file or retry after ${owner} finishes writing it.`);
|
|
83
|
+
this.name = 'EmptySharedConfigError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Refuse to read or write through a symlink at an adapter-owned path. */
|
|
87
|
+
export function assertNotSymlink(path, label) {
|
|
88
|
+
let stat;
|
|
89
|
+
try {
|
|
90
|
+
stat = lstatSync(path);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === 'ENOENT')
|
|
94
|
+
return;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
if (stat.isSymbolicLink())
|
|
98
|
+
throw new SymlinkRefusedError(label, path);
|
|
99
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { InjectionPlan, McpServerCmd, RuntimeId } from './injection.js';
|
|
2
|
-
import { type InstallOptions, type InstallResult, type UninstallOptions } from './
|
|
2
|
+
import { type InstallOptions, type InstallResult, type UninstallOptions } from './installerShared.js';
|
|
3
3
|
type BeforeReplaceHook = (path: string) => void;
|
|
4
4
|
export declare function _setJsonMcpBeforeReplaceHookForTest(hook?: BeforeReplaceHook): void;
|
|
5
5
|
export declare function _setJsonMcpAfterReplaceHookForTest(hook?: BeforeReplaceHook): void;
|
|
6
|
+
export declare function _setJsonMcpAfterSharedFileValidateHookForTest(hook?: BeforeReplaceHook): void;
|
|
6
7
|
export declare function _setJsonMcpBeforeBackupRemoveHookForTest(hook?: BeforeReplaceHook): void;
|
|
8
|
+
export declare function _setJsonMcpBeforeSharedFileCommitHookForTest(hook?: BeforeReplaceHook): void;
|
|
7
9
|
export interface JsonMcpRuntimeSpec {
|
|
8
10
|
runtime: 'opencode' | 'kiro';
|
|
9
11
|
configPath(opts: JsonMcpPathOptions): string;
|
|
@@ -70,6 +72,8 @@ export declare class McpConfigChangedError extends Error {
|
|
|
70
72
|
export declare class McpServerValidationError extends Error {
|
|
71
73
|
constructor(message: string);
|
|
72
74
|
}
|
|
75
|
+
declare function retrySharedConfigTransaction<T>(operation: () => T): T;
|
|
76
|
+
export declare const _retrySharedConfigTransactionForTest: typeof retrySharedConfigTransaction;
|
|
73
77
|
export declare function installJsonMcpRuntime(spec: JsonMcpRuntimeSpec, plan: InjectionPlan, opts: InstallOptions): InstallResult;
|
|
74
78
|
export declare function uninstallJsonMcpRuntime(spec: JsonMcpRuntimeSpec, runtime: RuntimeId, opts: UninstallOptions): InstallResult;
|
|
75
79
|
export {};
|
package/dist/jsonMcpInstaller.js
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync,
|
|
1
|
+
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import { EmptySharedConfigError, SymlinkRefusedError, UnparseableConfigError, } from './
|
|
5
|
+
import { EmptySharedConfigError, SymlinkRefusedError, UnparseableConfigError, } from './installerShared.js';
|
|
6
|
+
import { commitSharedFile, SharedFileConflictError } from './sharedFileCommit.js';
|
|
6
7
|
const CONFIG_MODE = 0o600;
|
|
7
8
|
const DIR_MODE = 0o700;
|
|
8
9
|
const BACKUP_VERSION = 1;
|
|
9
10
|
const ENV_FILE_KEY = 'EVOLVER_ENV_FILE';
|
|
11
|
+
const SHARED_CONFIG_WRITE_RETRIES = 5;
|
|
10
12
|
let beforeReplaceHookForTest;
|
|
11
13
|
let afterReplaceHookForTest;
|
|
14
|
+
let afterSharedFileValidateHookForTest;
|
|
12
15
|
let beforeBackupRemoveHookForTest;
|
|
16
|
+
let beforeSharedFileCommitHookForTest;
|
|
13
17
|
export function _setJsonMcpBeforeReplaceHookForTest(hook) {
|
|
14
18
|
beforeReplaceHookForTest = hook;
|
|
15
19
|
}
|
|
16
20
|
export function _setJsonMcpAfterReplaceHookForTest(hook) {
|
|
17
21
|
afterReplaceHookForTest = hook;
|
|
18
22
|
}
|
|
23
|
+
export function _setJsonMcpAfterSharedFileValidateHookForTest(hook) {
|
|
24
|
+
afterSharedFileValidateHookForTest = hook;
|
|
25
|
+
}
|
|
19
26
|
export function _setJsonMcpBeforeBackupRemoveHookForTest(hook) {
|
|
20
27
|
beforeBackupRemoveHookForTest = hook;
|
|
21
28
|
}
|
|
29
|
+
export function _setJsonMcpBeforeSharedFileCommitHookForTest(hook) {
|
|
30
|
+
beforeSharedFileCommitHookForTest = hook;
|
|
31
|
+
}
|
|
22
32
|
function removeBackup(path) {
|
|
23
33
|
beforeBackupRemoveHookForTest?.(path);
|
|
24
34
|
rmSync(path);
|
|
@@ -361,25 +371,20 @@ function assertConfigTopologyUnchanged(runtime, safeRoot, snapshot) {
|
|
|
361
371
|
throw new McpConfigChangedError(runtime, candidate.path);
|
|
362
372
|
}
|
|
363
373
|
}
|
|
364
|
-
function atomicWrite(path, raw, mode, beforeRename) {
|
|
365
|
-
mkdirSync(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
366
|
-
const tempPath = join(dirname(path), `.${randomUUID()}.tmp`);
|
|
367
|
-
try {
|
|
368
|
-
writeFileSync(tempPath, raw, { encoding: 'utf8', mode, flag: 'wx' });
|
|
369
|
-
chmodSync(tempPath, mode);
|
|
370
|
-
beforeRename?.();
|
|
371
|
-
renameSync(tempPath, path);
|
|
372
|
-
}
|
|
373
|
-
finally {
|
|
374
|
-
rmSync(tempPath, { force: true });
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
374
|
function guardedAtomicWrite(runtime, safeRoot, path, raw, mode, expectedRaw, additionalGuard) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
375
|
+
beforeReplaceHookForTest?.(path);
|
|
376
|
+
assertSafeParents(runtime, safeRoot, path);
|
|
377
|
+
assertUnchanged(runtime, path, expectedRaw);
|
|
378
|
+
additionalGuard?.();
|
|
379
|
+
commitSharedFile({
|
|
380
|
+
path,
|
|
381
|
+
expectedRaw: expectedRaw ?? undefined,
|
|
382
|
+
nextRaw: raw,
|
|
383
|
+
mode,
|
|
384
|
+
...(afterSharedFileValidateHookForTest ? { afterValidateForTest: () => afterSharedFileValidateHookForTest?.(path) } : {}),
|
|
385
|
+
...(beforeSharedFileCommitHookForTest
|
|
386
|
+
? { beforeCommitForTest: () => beforeSharedFileCommitHookForTest?.(path) }
|
|
387
|
+
: {}),
|
|
383
388
|
});
|
|
384
389
|
}
|
|
385
390
|
function guardedRemove(runtime, safeRoot, path, expectedRaw, additionalGuard) {
|
|
@@ -387,7 +392,15 @@ function guardedRemove(runtime, safeRoot, path, expectedRaw, additionalGuard) {
|
|
|
387
392
|
assertSafeParents(runtime, safeRoot, path);
|
|
388
393
|
assertUnchanged(runtime, path, expectedRaw);
|
|
389
394
|
additionalGuard?.();
|
|
390
|
-
|
|
395
|
+
commitSharedFile({
|
|
396
|
+
path,
|
|
397
|
+
expectedRaw: expectedRaw ?? undefined,
|
|
398
|
+
nextRaw: undefined,
|
|
399
|
+
...(afterSharedFileValidateHookForTest ? { afterValidateForTest: () => afterSharedFileValidateHookForTest?.(path) } : {}),
|
|
400
|
+
...(beforeSharedFileCommitHookForTest
|
|
401
|
+
? { beforeCommitForTest: () => beforeSharedFileCommitHookForTest?.(path) }
|
|
402
|
+
: {}),
|
|
403
|
+
});
|
|
391
404
|
}
|
|
392
405
|
function backupPath(configPath) {
|
|
393
406
|
return `${configPath}.evolver-backup.json`;
|
|
@@ -552,7 +565,24 @@ function looksLikeEnvFilePointer(value) {
|
|
|
552
565
|
|| /^[.~$%]/.test(value)
|
|
553
566
|
|| /\.(?:env|dotenv)(?:$|[._-])/i.test(value);
|
|
554
567
|
}
|
|
568
|
+
function retrySharedConfigTransaction(operation) {
|
|
569
|
+
for (let attempt = 1;; attempt += 1) {
|
|
570
|
+
try {
|
|
571
|
+
return operation();
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (!(error instanceof SharedFileConflictError)
|
|
575
|
+
|| error.recoveryPath !== undefined
|
|
576
|
+
|| attempt >= SHARED_CONFIG_WRITE_RETRIES)
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
export const _retrySharedConfigTransactionForTest = retrySharedConfigTransaction;
|
|
555
582
|
export function installJsonMcpRuntime(spec, plan, opts) {
|
|
583
|
+
return retrySharedConfigTransaction(() => installJsonMcpRuntimeOnce(spec, plan, opts));
|
|
584
|
+
}
|
|
585
|
+
function installJsonMcpRuntimeOnce(spec, plan, opts) {
|
|
556
586
|
validateServer(opts.server);
|
|
557
587
|
const resolution = resolveInstallRuntimeConfig(spec, opts);
|
|
558
588
|
const { configPath, safeRoot } = resolution;
|
|
@@ -707,6 +737,9 @@ function withoutEvolver(data, containerKey) {
|
|
|
707
737
|
return next;
|
|
708
738
|
}
|
|
709
739
|
export function uninstallJsonMcpRuntime(spec, runtime, opts) {
|
|
740
|
+
return retrySharedConfigTransaction(() => uninstallJsonMcpRuntimeOnce(spec, runtime, opts));
|
|
741
|
+
}
|
|
742
|
+
function uninstallJsonMcpRuntimeOnce(spec, runtime, opts) {
|
|
710
743
|
const resolution = resolveUninstallRuntimeConfig(spec, opts);
|
|
711
744
|
const { configPath, safeRoot } = resolution;
|
|
712
745
|
assertSafeParents(spec.runtime, safeRoot, configPath);
|
package/dist/kiroInstaller.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { InstallOptions } from './
|
|
1
|
+
import type { InstallOptions } from './installerShared.js';
|
|
2
2
|
import { type JsonMcpRuntimeResolution, type JsonMcpRuntimeSpec } from './jsonMcpInstaller.js';
|
|
3
3
|
type KiroPathOptions = Pick<InstallOptions, 'configRoot' | 'scope' | 'homeDir' | 'kiroHome'>;
|
|
4
4
|
/** Resolve only Kiro's base mcp.json contract; custom agent JSON is intentionally out of scope. */
|
|
5
5
|
export declare function resolveKiroConfig(opts: KiroPathOptions): JsonMcpRuntimeResolution;
|
|
6
6
|
export declare const KIRO_SPEC: JsonMcpRuntimeSpec;
|
|
7
7
|
export declare function kiroConfigRoot({ configRoot, scope, homeDir, kiroHome }: Pick<InstallOptions, 'configRoot' | 'scope' | 'homeDir' | 'kiroHome'>): string;
|
|
8
|
-
export declare const installKiro: (plan: import("./injection.js").InjectionPlan, opts: InstallOptions) => import("./
|
|
9
|
-
export declare const uninstallKiro: (runtime: import("./injection.js").RuntimeId, opts: import("./
|
|
8
|
+
export declare const installKiro: (plan: import("./injection.js").InjectionPlan, opts: InstallOptions) => import("./installerShared.js").InstallResult;
|
|
9
|
+
export declare const uninstallKiro: (runtime: import("./injection.js").RuntimeId, opts: import("./installerShared.js").UninstallOptions) => import("./installerShared.js").InstallResult;
|
|
10
10
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstallOptions } from './
|
|
1
|
+
import type { InstallOptions } from './installerShared.js';
|
|
2
2
|
import { type JsonMcpRuntimeResolution, type JsonMcpRuntimeSpec } from './jsonMcpInstaller.js';
|
|
3
3
|
type OpenCodePathOptions = Pick<InstallOptions, 'configRoot' | 'scope' | 'homeDir' | 'xdgConfigHome' | 'opencodeConfig' | 'opencodeConfigDir'>;
|
|
4
4
|
type OpenCodeManagedPathOptions = Pick<InstallOptions, 'opencodePlatform' | 'opencodeProgramData' | 'opencodeUsername'>;
|
|
@@ -13,6 +13,6 @@ export declare function resolveOpenCodeManagedPreferencePaths(opts?: OpenCodeMan
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function resolveOpenCodeConfig(opts: OpenCodePathOptions): JsonMcpRuntimeResolution;
|
|
15
15
|
export declare const OPENCODE_SPEC: JsonMcpRuntimeSpec;
|
|
16
|
-
export declare const installOpenCode: (plan: import("./injection.js").InjectionPlan, opts: InstallOptions) => import("./
|
|
17
|
-
export declare const uninstallOpenCode: (runtime: import("./injection.js").RuntimeId, opts: import("./
|
|
16
|
+
export declare const installOpenCode: (plan: import("./injection.js").InjectionPlan, opts: InstallOptions) => import("./installerShared.js").InstallResult;
|
|
17
|
+
export declare const uninstallOpenCode: (runtime: import("./injection.js").RuntimeId, opts: import("./installerShared.js").UninstallOptions) => import("./installerShared.js").InstallResult;
|
|
18
18
|
export {};
|
package/dist/proxyClient.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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 {
|
|
@@ -51,7 +52,11 @@ export class EvolverProxyClient {
|
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
submitAsset(asset) {
|
|
54
|
-
|
|
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
|
+
}));
|
|
55
60
|
}
|
|
56
61
|
submitAssetBundle(bundle) {
|
|
57
62
|
return this.call('POST', '/asset/submit', this.modeBoundBody(bundle));
|
|
@@ -221,12 +226,7 @@ function expectedHubModeFromEnv(env) {
|
|
|
221
226
|
return value === 'public' || value === 'private' ? value : undefined;
|
|
222
227
|
}
|
|
223
228
|
function readProxySettings(env, allowDefaultHome) {
|
|
224
|
-
const
|
|
225
|
-
const settingsDir = env['EVOLVER_SETTINGS_DIR']?.trim();
|
|
226
|
-
const homeDir = env['HOME']?.trim() || (allowDefaultHome ? homedir() : '');
|
|
227
|
-
const settingsPath = explicitPath
|
|
228
|
-
|| (settingsDir ? join(settingsDir, 'settings.json') : undefined)
|
|
229
|
-
|| (homeDir ? join(homeDir, '.evolver', 'settings.json') : undefined);
|
|
229
|
+
const settingsPath = resolveProxySettingsPath(env, allowDefaultHome);
|
|
230
230
|
if (!settingsPath)
|
|
231
231
|
return undefined;
|
|
232
232
|
try {
|
|
@@ -244,6 +244,16 @@ function readProxySettings(env, allowDefaultHome) {
|
|
|
244
244
|
return undefined;
|
|
245
245
|
}
|
|
246
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
|
+
}
|
|
247
257
|
function isLoopbackHttpUrl(raw) {
|
|
248
258
|
try {
|
|
249
259
|
const url = new URL(raw);
|
|
@@ -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
|
@@ -9,6 +9,7 @@ import { buildEvolverPrimer } from './primer.js';
|
|
|
9
9
|
import { EvolverMcpServer, UnknownToolError } from './server.js';
|
|
10
10
|
import { reachableProxyClientFromEnv } from './proxyClient.js';
|
|
11
11
|
import { loadEnvFileFromEnvOrThrow } from './envFile.js';
|
|
12
|
+
import { bootstrap } from '@evomap/evolver-core';
|
|
12
13
|
try {
|
|
13
14
|
loadEnvFileFromEnvOrThrow(process.env);
|
|
14
15
|
}
|
|
@@ -16,6 +17,8 @@ catch {
|
|
|
16
17
|
process.stderr.write('[evolver-mcp] fatal: failed to load EVOLVER_ENV_FILE\n');
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
20
|
+
// Emit deprecation warnings for any V1 env vars still present in the environment.
|
|
21
|
+
bootstrap.checkV1EnvCompat(process.env);
|
|
19
22
|
const store = new assetstore.LocalJsonlProvider(events.assetsDir());
|
|
20
23
|
const mailboxPath = process.env['EVOLVER_MCP_MAILBOX'] ?? join(events.evomapHome(), 'mailbox', 'mcp.db');
|
|
21
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.
|
|
3
|
+
"version": "2.0.0-beta.19",
|
|
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.
|
|
26
|
+
"@evomap/evolver-core": "2.0.0-beta.19",
|
|
24
27
|
"smol-toml": "^1.6.1"
|
|
25
28
|
},
|
|
26
29
|
"repository": {
|