@evomap/evolver-mcp 2.0.0-beta.1 → 2.0.0-beta.4

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.
@@ -0,0 +1,32 @@
1
+ import type { InjectionPlan, McpServerCmd, RuntimeId } from './injection.js';
2
+ import { type InstallOptions, type InstallResult, type UninstallOptions } from './installer.js';
3
+ export declare const ANTIGRAVITY_NAMESPACES: readonly ["antigravity", "antigravity-ide"];
4
+ export interface AntigravityConfigTarget {
5
+ namespace: (typeof ANTIGRAVITY_NAMESPACES)[number];
6
+ root: string;
7
+ configPath: string;
8
+ }
9
+ export declare class AntigravityConfigShapeError extends Error {
10
+ constructor(path: string, detail: string);
11
+ }
12
+ export declare class AntigravityPathTypeError extends Error {
13
+ constructor(label: string, path: string);
14
+ }
15
+ export declare class AntigravitySecretRefusedError extends Error {
16
+ constructor(detail: string);
17
+ }
18
+ /**
19
+ * Resolve Antigravity's user-level config targets. Every existing runtime root is targeted. If neither namespace
20
+ * exists, installation falls back to the canonical ~/.gemini/antigravity root. This function never creates paths.
21
+ */
22
+ export declare function resolveAntigravityConfigTargets(homeDir?: string): AntigravityConfigTarget[];
23
+ export declare function antigravityMcpServerEntry(server: McpServerCmd): Record<string, unknown>;
24
+ export declare function mergeAntigravityConfig(current: Record<string, unknown>, serverEntry: Record<string, unknown>): Record<string, unknown>;
25
+ export declare function stripAntigravityManaged(current: Record<string, unknown>): {
26
+ changed: boolean;
27
+ data: Record<string, unknown>;
28
+ };
29
+ /** Install MCP tool discovery only. Antigravity has no verified SessionStart hook contract. */
30
+ export declare function installAntigravity(plan: InjectionPlan, opts: InstallOptions): InstallResult;
31
+ /** Remove only mcpServers.evolver. The shared config file and all unrelated user content always remain. */
32
+ export declare function uninstallAntigravity(runtime: RuntimeId, opts: UninstallOptions): InstallResult;
@@ -0,0 +1,271 @@
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { homedir as osHomedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { util } from '@evomap/evolver-core';
6
+ import { EmptySharedConfigError, SymlinkRefusedError, UnparseableConfigError, } from './installer.js';
7
+ const ENV_FILE_KEY = 'EVOLVER_ENV_FILE';
8
+ const CONFIG_FILE = 'mcp_config.json';
9
+ const CONFIG_WRITE_RETRIES = 5;
10
+ const NEW_CONFIG_MODE = 0o600;
11
+ const NEW_DIR_MODE = 0o700;
12
+ export const ANTIGRAVITY_NAMESPACES = ['antigravity', 'antigravity-ide'];
13
+ export class AntigravityConfigShapeError extends Error {
14
+ constructor(path, detail) {
15
+ super(`[setup-hooks] refusing to overwrite Antigravity MCP config (${path}): ${detail}. Fix the shared config, then rerun.`);
16
+ this.name = 'AntigravityConfigShapeError';
17
+ }
18
+ }
19
+ export class AntigravityPathTypeError extends Error {
20
+ constructor(label, path) {
21
+ super(`[setup-hooks] refusing to operate: ${label} (${path}) exists but is not a directory.`);
22
+ this.name = 'AntigravityPathTypeError';
23
+ }
24
+ }
25
+ export class AntigravitySecretRefusedError extends Error {
26
+ constructor(detail) {
27
+ super(`[setup-hooks] refusing to write Antigravity MCP config: ${detail}; use only an ${ENV_FILE_KEY} pointer to a credential file.`);
28
+ this.name = 'AntigravitySecretRefusedError';
29
+ }
30
+ }
31
+ const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
32
+ function lstatIfExists(path) {
33
+ try {
34
+ return lstatSync(path);
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return undefined;
39
+ throw error;
40
+ }
41
+ }
42
+ function assertDirectoryOrMissing(path, label) {
43
+ const stat = lstatIfExists(path);
44
+ if (stat === undefined)
45
+ return false;
46
+ if (stat.isSymbolicLink())
47
+ throw new SymlinkRefusedError(label, path);
48
+ if (!stat.isDirectory())
49
+ throw new AntigravityPathTypeError(label, path);
50
+ return true;
51
+ }
52
+ function assertFileNotSymlink(path, label) {
53
+ const stat = lstatIfExists(path);
54
+ if (stat?.isSymbolicLink())
55
+ throw new SymlinkRefusedError(label, path);
56
+ }
57
+ /**
58
+ * Resolve Antigravity's user-level config targets. Every existing runtime root is targeted. If neither namespace
59
+ * exists, installation falls back to the canonical ~/.gemini/antigravity root. This function never creates paths.
60
+ */
61
+ export function resolveAntigravityConfigTargets(homeDir = osHomedir()) {
62
+ assertDirectoryOrMissing(homeDir, 'home directory');
63
+ const geminiRoot = join(homeDir, '.gemini');
64
+ assertDirectoryOrMissing(geminiRoot, '~/.gemini');
65
+ const candidates = ANTIGRAVITY_NAMESPACES.map((namespace) => {
66
+ const root = join(geminiRoot, namespace);
67
+ return { namespace, root, configPath: join(root, CONFIG_FILE) };
68
+ });
69
+ const existing = candidates.filter((target) => assertDirectoryOrMissing(target.root, `~/.gemini/${target.namespace}`));
70
+ const targets = existing.length > 0 ? existing : [candidates[0]];
71
+ for (const target of targets) {
72
+ assertFileNotSymlink(target.configPath, `~/.gemini/${target.namespace}/${CONFIG_FILE}`);
73
+ }
74
+ return targets;
75
+ }
76
+ function validateConfigShape(data, path) {
77
+ const mcpServers = data['mcpServers'];
78
+ if (mcpServers !== undefined && !isObj(mcpServers)) {
79
+ throw new AntigravityConfigShapeError(path, 'mcpServers must be a JSON object');
80
+ }
81
+ }
82
+ function readStrictSnapshot(path) {
83
+ let raw;
84
+ try {
85
+ raw = readFileSync(path, 'utf8');
86
+ }
87
+ catch (error) {
88
+ if (error.code === 'ENOENT')
89
+ return { data: {}, raw: null };
90
+ throw error;
91
+ }
92
+ const trimmed = raw.trim();
93
+ if (!trimmed)
94
+ throw new EmptySharedConfigError('Antigravity MCP config', path, 'Antigravity');
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(trimmed);
98
+ }
99
+ catch {
100
+ throw new UnparseableConfigError('Antigravity MCP config', path, 'Antigravity');
101
+ }
102
+ if (!isObj(parsed)) {
103
+ throw new AntigravityConfigShapeError(path, 'the top-level JSON value must be an object');
104
+ }
105
+ validateConfigShape(parsed, path);
106
+ return { data: parsed, raw };
107
+ }
108
+ function readRawIfExists(path) {
109
+ try {
110
+ return readFileSync(path, 'utf8');
111
+ }
112
+ catch (error) {
113
+ if (error.code === 'ENOENT')
114
+ return null;
115
+ throw error;
116
+ }
117
+ }
118
+ function existingFileMode(path) {
119
+ try {
120
+ return statSync(path).mode & 0o777;
121
+ }
122
+ catch (error) {
123
+ if (error.code === 'ENOENT')
124
+ return undefined;
125
+ throw error;
126
+ }
127
+ }
128
+ function writeJsonAtomic(path, data) {
129
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
130
+ const mode = existingFileMode(path) ?? NEW_CONFIG_MODE;
131
+ const restoreMode = process.platform === 'win32' && existsSync(path) ? existingFileMode(path) : undefined;
132
+ try {
133
+ writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode });
134
+ chmodSync(tmp, mode);
135
+ if (process.platform === 'win32' && existsSync(path))
136
+ chmodSync(path, mode | 0o200);
137
+ renameSync(tmp, path);
138
+ chmodSync(path, mode);
139
+ }
140
+ catch (error) {
141
+ rmSync(tmp, { force: true });
142
+ if (restoreMode !== undefined) {
143
+ try {
144
+ if (existsSync(path))
145
+ chmodSync(path, restoreMode);
146
+ }
147
+ catch { /* preserve the original error */ }
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ function updateConfigWithRetry(path, update) {
153
+ const lockPath = `${path}.evolver.lock`;
154
+ util.acquireLock(lockPath);
155
+ try {
156
+ for (let attempt = 0; attempt < CONFIG_WRITE_RETRIES; attempt++) {
157
+ const snapshot = readStrictSnapshot(path);
158
+ const next = update(snapshot.data);
159
+ if (!next.changed)
160
+ return false;
161
+ if (readRawIfExists(path) !== snapshot.raw)
162
+ continue;
163
+ writeJsonAtomic(path, next.data);
164
+ return true;
165
+ }
166
+ }
167
+ finally {
168
+ util.releaseLock(lockPath);
169
+ }
170
+ throw new Error(`[setup-hooks] refusing to overwrite Antigravity MCP config (${path}): the file changed repeatedly while evolver was merging it. Retry after Antigravity finishes writing it.`);
171
+ }
172
+ const SECRET_FLAG_RE = /(?:^|\s)--?(?:api[-_]?key|password|passwd|secret|token)(?:=|\s+)/i;
173
+ const SECRET_ASSIGNMENT_RE = /\b(?:A2A_NODE_SECRET|EVOMAP_ENTERPRISE_TOKEN|EVOMAP_PRIVATE_HUB_TOKEN|EVOMAP_NODE_SECRET|EVOLVER_IPC_TOKEN|EVOLVER_LLM_TOKEN|PHUB_ENTERPRISE_TOKEN|PRIVATE_HUB_ENTERPRISE_TOKEN)\b\s*=/;
174
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/i;
175
+ function looksLikeEnvFilePointer(value) {
176
+ return /[\\/]/.test(value)
177
+ || /^[.~$%]/.test(value)
178
+ || /\.(?:env|dotenv)(?:$|[._-])/i.test(value);
179
+ }
180
+ function validateServer(server) {
181
+ const commandLine = [server.command, ...(server.args ?? [])].join(' ');
182
+ if (SECRET_FLAG_RE.test(commandLine) || SECRET_ASSIGNMENT_RE.test(commandLine) || BEARER_RE.test(commandLine)) {
183
+ throw new AntigravitySecretRefusedError('the MCP command or args contain an inline secret-looking value');
184
+ }
185
+ if (server.env === undefined || Object.keys(server.env).length === 0)
186
+ return;
187
+ const keys = Object.keys(server.env);
188
+ if (keys.some((key) => key !== ENV_FILE_KEY)) {
189
+ throw new AntigravitySecretRefusedError(`env may contain only ${ENV_FILE_KEY}`);
190
+ }
191
+ const pointer = server.env[ENV_FILE_KEY]?.trim();
192
+ if (!pointer || !looksLikeEnvFilePointer(pointer)) {
193
+ throw new AntigravitySecretRefusedError(`${ENV_FILE_KEY} must contain a file path, not a credential value`);
194
+ }
195
+ }
196
+ export function antigravityMcpServerEntry(server) {
197
+ validateServer(server);
198
+ const pointer = server.env?.[ENV_FILE_KEY]?.trim();
199
+ return {
200
+ command: server.command,
201
+ args: server.args ?? [],
202
+ ...(pointer ? { env: { [ENV_FILE_KEY]: pointer } } : {}),
203
+ };
204
+ }
205
+ export function mergeAntigravityConfig(current, serverEntry) {
206
+ const prior = current['mcpServers'];
207
+ const mcpServers = isObj(prior) ? { ...prior } : {};
208
+ mcpServers['evolver'] = serverEntry;
209
+ return { ...current, mcpServers };
210
+ }
211
+ export function stripAntigravityManaged(current) {
212
+ const prior = current['mcpServers'];
213
+ if (!isObj(prior) || !('evolver' in prior))
214
+ return { changed: false, data: current };
215
+ const mcpServers = { ...prior };
216
+ delete mcpServers['evolver'];
217
+ const data = { ...current };
218
+ if (Object.keys(mcpServers).length > 0)
219
+ data['mcpServers'] = mcpServers;
220
+ else
221
+ delete data['mcpServers'];
222
+ return { changed: true, data };
223
+ }
224
+ function hasEvolver(current) {
225
+ const mcpServers = current['mcpServers'];
226
+ return isObj(mcpServers) && 'evolver' in mcpServers;
227
+ }
228
+ function ensureTargetRoot(target) {
229
+ const geminiRoot = dirname(target.root);
230
+ assertDirectoryOrMissing(geminiRoot, '~/.gemini');
231
+ mkdirSync(dirname(target.configPath), { recursive: true, mode: NEW_DIR_MODE });
232
+ assertDirectoryOrMissing(geminiRoot, '~/.gemini');
233
+ assertDirectoryOrMissing(target.root, `~/.gemini/${target.namespace}`);
234
+ assertFileNotSymlink(target.configPath, `~/.gemini/${target.namespace}/${CONFIG_FILE}`);
235
+ }
236
+ /** Install MCP tool discovery only. Antigravity has no verified SessionStart hook contract. */
237
+ export function installAntigravity(plan, opts) {
238
+ const serverEntry = antigravityMcpServerEntry(opts.server);
239
+ const targets = resolveAntigravityConfigTargets(opts.homeDir);
240
+ // Validate every shared config before the first mutation so one corrupt namespace cannot leave a partial install.
241
+ const snapshots = targets.map((target) => ({ target, snapshot: readStrictSnapshot(target.configPath) }));
242
+ if (!opts.force && snapshots.every(({ snapshot }) => hasEvolver(snapshot.data))) {
243
+ return { ok: true, runtime: plan.runtime, mode: plan.mode, files: [], alreadyInstalled: true };
244
+ }
245
+ const files = [];
246
+ for (const { target } of snapshots) {
247
+ ensureTargetRoot(target);
248
+ const changed = updateConfigWithRetry(target.configPath, (current) => {
249
+ if (!opts.force && hasEvolver(current))
250
+ return { changed: false, data: current };
251
+ return { changed: true, data: mergeAntigravityConfig(current, serverEntry) };
252
+ });
253
+ if (changed)
254
+ files.push(target.configPath);
255
+ }
256
+ return { ok: true, runtime: plan.runtime, mode: plan.mode, files };
257
+ }
258
+ /** Remove only mcpServers.evolver. The shared config file and all unrelated user content always remain. */
259
+ export function uninstallAntigravity(runtime, opts) {
260
+ const targets = resolveAntigravityConfigTargets(opts.homeDir);
261
+ const snapshots = targets.map((target) => ({ target, snapshot: readStrictSnapshot(target.configPath) }));
262
+ const files = [];
263
+ for (const { target, snapshot } of snapshots) {
264
+ if (!hasEvolver(snapshot.data))
265
+ continue;
266
+ const changed = updateConfigWithRetry(target.configPath, stripAntigravityManaged);
267
+ if (changed)
268
+ files.push(target.configPath);
269
+ }
270
+ return { ok: true, runtime, mode: 'uninstall', files };
271
+ }
package/dist/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export * from './manualWiring.js';
9
9
  export * from './serviceGuidance.js';
10
10
  export * from './codexInstaller.js';
11
11
  export * from './cursorRulesInstaller.js';
12
+ export * from './antigravityInstaller.js';
12
13
  export * from './envFile.js';
package/dist/index.js CHANGED
@@ -9,4 +9,5 @@ export * from './manualWiring.js';
9
9
  export * from './serviceGuidance.js';
10
10
  export * from './codexInstaller.js';
11
11
  export * from './cursorRulesInstaller.js';
12
+ export * from './antigravityInstaller.js';
12
13
  export * from './envFile.js';
@@ -1,10 +1,10 @@
1
- export type RuntimeId = 'claude-code' | 'codex' | 'cursor' | 'kiro' | 'opencode';
2
- export type InjectionMode = 'mcp-hooks' | 'mcp-plugin' | 'cursor-rules' | 'passive';
1
+ export type RuntimeId = 'claude-code' | 'codex' | 'cursor' | 'antigravity' | 'kiro' | 'opencode';
2
+ export type InjectionMode = 'mcp-hooks' | 'mcp-plugin' | 'mcp-config' | 'cursor-rules' | 'passive';
3
3
  /**
4
4
  * Setup support contract (#217). A bootstrapper that delegates runtime onboarding to evolver v2 needs a
5
5
  * DETERMINISTIC answer for every runtime it might ask about — not just the ones v2 can write config for.
6
6
  * The three outcomes are the whole contract:
7
- * - `installed` v2 can write the runtime config/hooks and verify it (claude-code, codex, cursor today).
7
+ * - `installed` v2 can write the runtime config/hooks and verify it (claude-code, codex, cursor, antigravity).
8
8
  * - `manual` v2 cannot mutate this runtime's config, but the path is real: it prints precise MCP/HTTP
9
9
  * wiring the operator does by hand (opencode, openclaw, mcp-generic, http-agent, server).
10
10
  * - `unsupported` v2 refuses with a clear reason (an unrecognized runtime id).
package/dist/injection.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /** Every runtime in the setup matrix (#217), in a stable order — used for usage text and the unsupported reason. */
2
2
  export const SETUP_RUNTIMES = [
3
- 'claude-code', 'codex', 'cursor', 'opencode', 'kiro', 'openclaw', 'mcp-generic', 'http-agent', 'server',
3
+ 'claude-code', 'codex', 'cursor', 'antigravity', 'opencode', 'kiro', 'openclaw', 'mcp-generic', 'http-agent', 'server',
4
4
  ];
5
- /** Runtimes v2 can write config/hooks for and verify. */
6
- const INSTALLED_RUNTIMES = new Set(['claude-code', 'codex', 'cursor']);
5
+ /** Runtimes v2 can write config/hooks for and verify. Antigravity is MCP-config-only (no lifecycle hook). */
6
+ const INSTALLED_RUNTIMES = new Set(['claude-code', 'codex', 'cursor', 'antigravity']);
7
7
  /** Runtimes with no v2 auto-installer but a real manual path. The reason is the short, honest "do it by hand"
8
8
  * line; the precise wiring text is a separate concern (#217 slice 2), not hard-coded here. */
9
9
  const MANUAL_RUNTIMES = new Map([
@@ -59,6 +59,14 @@ export function planInjection(runtime, server) {
59
59
  config: {},
60
60
  note: 'cursor: 渲染静默 top-gene hints 进 .cursor/rules/evolver.mdc (alwaysApply:true); daemon 在 gene 集变化时重写',
61
61
  };
62
+ case 'antigravity':
63
+ return {
64
+ runtime, mode: 'mcp-config',
65
+ // Antigravity supports MCP tool discovery through its user-level JSON config. It has no verified
66
+ // SessionStart lifecycle contract, so this mode deliberately registers only the MCP server.
67
+ config: { mcpServers: { evolver: { command: server.command, args: server.args ?? [], ...(server.env ? { env: server.env } : {}) } } },
68
+ note: 'antigravity: write mcpServers.evolver in the user-level MCP config; tool discovery only (no SessionStart hook)',
69
+ };
62
70
  case 'kiro':
63
71
  case 'opencode':
64
72
  return {
@@ -76,7 +84,7 @@ export function planInjection(runtime, server) {
76
84
  * 故不计入此处;passive runtime 也为 false. */
77
85
  export function injectsTools(runtime) {
78
86
  const mode = planInjection(runtime, { command: 'x' }).mode;
79
- return mode === 'mcp-hooks' || mode === 'mcp-plugin';
87
+ return mode === 'mcp-hooks' || mode === 'mcp-plugin' || mode === 'mcp-config';
80
88
  }
81
89
  /** 是否为 active 注入(任何把 gene 价值推回 runtime 的方式:MCP 工具发现 或 cursor rules 记忆注入). */
82
90
  export function isActiveInjection(runtime) {
@@ -37,6 +37,15 @@ export interface InstallOptions {
37
37
  genes?: readonly CursorGene[];
38
38
  /** Cursor only: cap on genes rendered into the always-on rules body (token-tax bound). */
39
39
  maxGenes?: number;
40
+ /** Antigravity only: override the user home used to resolve ~/.gemini config roots. Intended for hermetic
41
+ * embedding/tests; normal callers omit it. configRoot and scope do not affect Antigravity's user config. */
42
+ homeDir?: string;
43
+ }
44
+ export interface UninstallOptions {
45
+ configRoot: string;
46
+ scope?: InstallScope;
47
+ /** Antigravity only: override the user home used to resolve ~/.gemini config roots. */
48
+ homeDir?: string;
40
49
  }
41
50
  export interface InstallResult {
42
51
  ok: boolean;
@@ -60,14 +69,14 @@ export declare class SymlinkRefusedError extends Error {
60
69
  * .mcp.json/.claude/settings.json are evolver-owned, so their lenient fresh-start behavior stays unchanged.
61
70
  */
62
71
  export declare class UnparseableConfigError extends Error {
63
- constructor(label: string, path: string);
72
+ constructor(label: string, path: string, owner?: string);
64
73
  }
65
74
  /**
66
75
  * Thrown when a SHARED user config exists but is empty or whitespace-only. Claude Code writes these files with a
67
76
  * truncating write, so present-empty can be a concurrent-write window rather than a fresh config.
68
77
  */
69
78
  export declare class EmptySharedConfigError extends Error {
70
- constructor(label: string, path: string);
79
+ constructor(label: string, path: string, owner?: string);
71
80
  }
72
81
  type SharedConfigRaceHook = (path: string, attempt: number) => void;
73
82
  export declare function _setSharedConfigRaceHookForTest(hook?: SharedConfigRaceHook): void;
@@ -90,6 +99,8 @@ export declare function stripManaged(data: Record<string, unknown>): {
90
99
  * (delegated to codexInstaller; TOML, not JSON). Same hybrid value (tool discovery + session-start injection).
91
100
  * - cursor (cursor-rules): renders top genes into <root>/.cursor/rules/evolver.mdc (alwaysApply:true) — gene
92
101
  * memory injection, not MCP tool discovery (delegated to cursorRulesInstaller). The daemon keeps it fresh.
102
+ * - antigravity (mcp-config): writes mcpServers.evolver to every existing user-level Antigravity config root,
103
+ * or the canonical root when none exists. MCP tool discovery only; no SessionStart hook is installed.
93
104
  * Idempotent + symlink-safe; passive runtimes (kiro/opencode) return ok:false (nothing to inject).
94
105
  */
95
106
  export declare function installInjection(plan: InjectionPlan, opts: InstallOptions): InstallResult;
@@ -97,10 +108,7 @@ export declare function installInjection(plan: InjectionPlan, opts: InstallOptio
97
108
  * Pass the SAME scope used at install: 'user' cleans ~/.claude.json + ~/.claude/settings.json; 'project'
98
109
  * (default) cleans <configRoot>/.mcp.json + <configRoot>/.claude/settings.json. stripManaged only removes
99
110
  * the mcpServers.evolver entry (and any evolver-owned hooks/marker), so it is safe on the shared ~/.claude.json. */
100
- export declare function uninstallInjection(runtime: RuntimeId, opts: {
101
- configRoot: string;
102
- scope?: InstallScope;
103
- }): InstallResult;
111
+ export declare function uninstallInjection(runtime: RuntimeId, opts: UninstallOptions): InstallResult;
104
112
  /** Convenience: plan + install in one call for a runtime. */
105
113
  export declare function setupRuntime(runtime: RuntimeId, opts: InstallOptions): InstallResult;
106
114
  export {};
package/dist/installer.js CHANGED
@@ -22,6 +22,7 @@ import { installCodex, uninstallCodex } from './codexInstaller.js';
22
22
  // cursor injection is a different mechanism again (a project rules file, not a config/MCP writer): it renders
23
23
  // top genes into .cursor/rules/evolver.mdc. It plugs into the same install/uninstall dispatch below.
24
24
  import { installCursorRules, uninstallCursorRules } from './cursorRulesInstaller.js';
25
+ import { installAntigravity, uninstallAntigravity } from './antigravityInstaller.js';
25
26
  /** Marks a config file as containing evolver-managed entries, so uninstall only removes what we added. */
26
27
  export const MANAGED_MARKER = '_evolver_managed';
27
28
  /** A hook entry is evolver-owned if any of its commands mention this — used to replace-not-duplicate on reinstall. */
@@ -49,8 +50,8 @@ export class SymlinkRefusedError extends Error {
49
50
  * .mcp.json/.claude/settings.json are evolver-owned, so their lenient fresh-start behavior stays unchanged.
50
51
  */
51
52
  export class UnparseableConfigError extends Error {
52
- constructor(label, path) {
53
- super(`[setup-hooks] refusing to overwrite ${label} (${path}): the file exists and is non-empty but is not valid JSON. This is Claude Code's own shared config; merging into it would replace the whole file and could wipe its contents (projects/oauthAccount/userID/history/settings). Fix or remove the corrupt file, then rerun.`);
53
+ constructor(label, path, owner = 'Claude Code') {
54
+ 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.`);
54
55
  this.name = 'UnparseableConfigError';
55
56
  }
56
57
  }
@@ -59,8 +60,8 @@ export class UnparseableConfigError extends Error {
59
60
  * truncating write, so present-empty can be a concurrent-write window rather than a fresh config.
60
61
  */
61
62
  export class EmptySharedConfigError extends Error {
62
- constructor(label, path) {
63
- super(`[setup-hooks] refusing to overwrite ${label} (${path}): the file exists but is empty or contains only whitespace. Claude Code 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 Claude Code finishes writing it.`);
63
+ constructor(label, path, owner = 'Claude Code') {
64
+ 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.`);
64
65
  this.name = 'EmptySharedConfigError';
65
66
  }
66
67
  }
@@ -386,6 +387,8 @@ function claudeCodeTargets(scope, configRoot) {
386
387
  * (delegated to codexInstaller; TOML, not JSON). Same hybrid value (tool discovery + session-start injection).
387
388
  * - cursor (cursor-rules): renders top genes into <root>/.cursor/rules/evolver.mdc (alwaysApply:true) — gene
388
389
  * memory injection, not MCP tool discovery (delegated to cursorRulesInstaller). The daemon keeps it fresh.
390
+ * - antigravity (mcp-config): writes mcpServers.evolver to every existing user-level Antigravity config root,
391
+ * or the canonical root when none exists. MCP tool discovery only; no SessionStart hook is installed.
389
392
  * Idempotent + symlink-safe; passive runtimes (kiro/opencode) return ok:false (nothing to inject).
390
393
  */
391
394
  export function installInjection(plan, opts) {
@@ -398,9 +401,12 @@ export function installInjection(plan, opts) {
398
401
  if (plan.runtime === 'cursor') {
399
402
  return installCursorRules({ configRoot: opts.configRoot, genes: opts.genes ?? [], ...(opts.maxGenes !== undefined ? { maxGenes: opts.maxGenes } : {}) });
400
403
  }
404
+ if (plan.runtime === 'antigravity') {
405
+ return installAntigravity(plan, opts);
406
+ }
401
407
  if (plan.runtime !== 'claude-code') {
402
408
  // kiro/opencode are passive (handled above); any other active runtime is not yet ported.
403
- return { ok: false, runtime: plan.runtime, mode: plan.mode, files: [], error: `installer not yet implemented for ${plan.runtime} (supported: claude-code, codex, cursor)` };
409
+ return { ok: false, runtime: plan.runtime, mode: plan.mode, files: [], error: `installer not yet implemented for ${plan.runtime} (supported: claude-code, codex, cursor, antigravity)` };
404
410
  }
405
411
  const hookCommand = opts.hookCommand ?? DEFAULT_HOOK_COMMAND;
406
412
  const scope = opts.scope ?? 'project';
@@ -478,6 +484,9 @@ export function uninstallInjection(runtime, opts) {
478
484
  if (runtime === 'cursor') {
479
485
  return uninstallCursorRules(opts);
480
486
  }
487
+ if (runtime === 'antigravity') {
488
+ return uninstallAntigravity(runtime, opts);
489
+ }
481
490
  if (runtime !== 'claude-code') {
482
491
  return { ok: false, runtime, mode: 'n/a', files: [], error: `uninstall not implemented for ${runtime}` };
483
492
  }
@@ -41,6 +41,20 @@ export interface ProxyReuseResultArgs {
41
41
  timeSavedSeconds?: number;
42
42
  reason?: string;
43
43
  }
44
+ export interface ProxyAgentSearchArgs {
45
+ query?: string;
46
+ signals?: string[];
47
+ availability?: string;
48
+ sort?: string;
49
+ order?: string;
50
+ cursor?: string;
51
+ limit?: number;
52
+ timeoutMs?: number;
53
+ }
54
+ export interface ProxyAgentDiscoverArgs extends ProxyAgentSearchArgs {
55
+ title: string;
56
+ description?: string;
57
+ }
44
58
  export declare class EvolverProxyClient {
45
59
  private baseUrl;
46
60
  private token;
@@ -52,6 +66,9 @@ export declare class EvolverProxyClient {
52
66
  }): Promise<unknown>;
53
67
  search(args: ProxySearchArgs): Promise<unknown>;
54
68
  fetchAsset(args: ProxyFetchArgs): Promise<unknown>;
69
+ searchAgents(args: ProxyAgentSearchArgs): Promise<unknown>;
70
+ getAgentProfile(agentId: string, timeoutMs?: number): Promise<unknown>;
71
+ discoverAgentsForTask(args: ProxyAgentDiscoverArgs): Promise<unknown>;
55
72
  submitAsset(asset: unknown): Promise<unknown>;
56
73
  /** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
57
74
  validateAsset(asset: unknown): Promise<unknown>;
@@ -31,6 +31,19 @@ export class EvolverProxyClient {
31
31
  ...(args.assetIds ? { asset_ids: args.assetIds } : {}),
32
32
  });
33
33
  }
34
+ searchAgents(args) {
35
+ return this.call('POST', '/agent/search', agentDirectoryBody(args));
36
+ }
37
+ getAgentProfile(agentId, timeoutMs) {
38
+ return this.call('POST', '/agent/profile', { agent_id: agentId, ...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}) });
39
+ }
40
+ discoverAgentsForTask(args) {
41
+ return this.call('POST', '/agent/discover', {
42
+ title: args.title,
43
+ ...(args.description ? { description: args.description } : {}),
44
+ ...agentDirectoryBody(args),
45
+ });
46
+ }
34
47
  submitAsset(asset) {
35
48
  return this.call('POST', '/asset/submit', { assets: [asset] });
36
49
  }
@@ -107,6 +120,18 @@ export class EvolverProxyClient {
107
120
  return new Error(message);
108
121
  }
109
122
  }
123
+ function agentDirectoryBody(args) {
124
+ return {
125
+ ...(args.query ? { query: args.query } : {}),
126
+ ...(args.signals && args.signals.length > 0 ? { signals: args.signals } : {}),
127
+ ...(args.availability ? { availability: args.availability } : {}),
128
+ ...(args.sort ? { sort: args.sort } : {}),
129
+ ...(args.order ? { order: args.order } : {}),
130
+ ...(args.cursor ? { cursor: args.cursor } : {}),
131
+ ...(args.limit !== undefined ? { limit: args.limit } : {}),
132
+ ...(args.timeoutMs !== undefined ? { timeout_ms: args.timeoutMs } : {}),
133
+ };
134
+ }
110
135
  export function proxyClientFromEnv(env = process.env) {
111
136
  const token = env['EVOLVER_IPC_TOKEN']?.trim();
112
137
  if (!token)
package/dist/tools.js CHANGED
@@ -375,6 +375,40 @@ export function buildEvolverTools(deps) {
375
375
  }
376
376
  if (deps.proxy) {
377
377
  tools.push({
378
+ name: 'evolver_agent_search',
379
+ description: '按自然语言 query 或 capability signals 搜索可协作 agent;结果来自 Hub,不代表实时可用,availability=unknown 时不得推断在线。',
380
+ inputSchema: agentDirectorySearchSchema(),
381
+ handler: async (a) => deps.proxy.searchAgents(agentSearchArgs(a)),
382
+ }, {
383
+ name: 'evolver_agent_profile',
384
+ description: '读取 Hub 授权返回的最小安全 agent profile;不返回凭证、node secret、workspace path 或设备指纹。',
385
+ inputSchema: {
386
+ type: 'object',
387
+ required: ['agentId'],
388
+ properties: {
389
+ agentId: { type: 'string', minLength: 1, maxLength: hub.AGENT_DIRECTORY_MAX_AGENT_ID_LENGTH },
390
+ timeoutMs: { type: 'integer', minimum: 100, maximum: hub.AGENT_DIRECTORY_MAX_TIMEOUT_MS },
391
+ },
392
+ },
393
+ handler: async (a) => deps.proxy.getAgentProfile(str(a['agentId']), typeof a['timeoutMs'] === 'number' ? a['timeoutMs'] : undefined),
394
+ }, {
395
+ name: 'evolver_agent_discover',
396
+ description: '按任务标题、描述和 capability signals 发现候选 agent;分页和排序由 Hub 执行。',
397
+ inputSchema: {
398
+ ...agentDirectorySearchSchema(),
399
+ required: ['title'],
400
+ properties: {
401
+ ...agentDirectorySearchSchema()['properties'],
402
+ title: { type: 'string', minLength: 1, maxLength: hub.AGENT_DIRECTORY_MAX_QUERY_LENGTH },
403
+ description: { type: 'string', maxLength: hub.AGENT_DIRECTORY_MAX_QUERY_LENGTH },
404
+ },
405
+ },
406
+ handler: async (a) => deps.proxy.discoverAgentsForTask({
407
+ title: str(a['title']),
408
+ ...(typeof a['description'] === 'string' ? { description: a['description'] } : {}),
409
+ ...agentSearchArgs(a),
410
+ }),
411
+ }, {
378
412
  name: 'evolver_asset_validate',
379
413
  description: '通过本机 evolver-proxy 对 PHub 做发布前 dry-run 校验: 先执行与发布相同的本地脱敏/泄漏拦截, 再跑 hub 端质量门禁 + 内容安全扫描, 不落库、不计费. 返回 {valid, reason?}. 建议在 evolver_asset_publish 前调用. Capsule.gene 须非空或 ad-hoc.',
380
414
  inputSchema: {
@@ -398,4 +432,31 @@ export function buildEvolverTools(deps) {
398
432
  });
399
433
  }
400
434
  return tools;
435
+ }
436
+ function agentDirectorySearchSchema() {
437
+ return {
438
+ type: 'object',
439
+ properties: {
440
+ query: { type: 'string', minLength: 1, maxLength: hub.AGENT_DIRECTORY_MAX_QUERY_LENGTH },
441
+ signals: { type: 'array', maxItems: hub.AGENT_DIRECTORY_MAX_SIGNAL_COUNT, items: { type: 'string', minLength: 1, maxLength: hub.AGENT_DIRECTORY_MAX_SIGNAL_LENGTH } },
442
+ availability: { type: 'string', enum: ['online', 'busy', 'offline', 'unknown'] },
443
+ sort: { type: 'string', enum: ['relevance', 'reputation', 'recent', 'availability'] },
444
+ order: { type: 'string', enum: ['asc', 'desc'] },
445
+ cursor: { type: 'string', maxLength: hub.AGENT_DIRECTORY_MAX_CURSOR_LENGTH },
446
+ limit: { type: 'integer', minimum: 1, maximum: hub.AGENT_DIRECTORY_MAX_LIMIT },
447
+ timeoutMs: { type: 'integer', minimum: 100, maximum: hub.AGENT_DIRECTORY_MAX_TIMEOUT_MS },
448
+ },
449
+ };
450
+ }
451
+ function agentSearchArgs(args) {
452
+ return {
453
+ ...(typeof args['query'] === 'string' ? { query: args['query'] } : {}),
454
+ ...(Array.isArray(args['signals']) ? { signals: strArray(args['signals']) ?? [] } : {}),
455
+ ...(typeof args['availability'] === 'string' ? { availability: args['availability'] } : {}),
456
+ ...(typeof args['sort'] === 'string' ? { sort: args['sort'] } : {}),
457
+ ...(typeof args['order'] === 'string' ? { order: args['order'] } : {}),
458
+ ...(typeof args['cursor'] === 'string' ? { cursor: args['cursor'] } : {}),
459
+ ...(typeof args['limit'] === 'number' ? { limit: args['limit'] } : {}),
460
+ ...(typeof args['timeoutMs'] === 'number' ? { timeoutMs: args['timeoutMs'] } : {}),
461
+ };
401
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-mcp",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Evolver MCP server (agent 工具发现入口)",
@@ -20,15 +20,20 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@evomap/evolver-core": "2.0.0-beta.1",
23
+ "@evomap/evolver-core": "2.0.0-beta.4",
24
24
  "smol-toml": "^1.6.1"
25
25
  },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/EvoMap/evolver.git"
29
+ },
26
30
  "publishConfig": {
27
31
  "access": "public",
28
32
  "tag": "v2-beta"
29
33
  },
30
34
  "files": [
31
35
  "dist/",
36
+ "assets/",
32
37
  "README.md",
33
38
  "package.json"
34
39
  ]