@evomap/evolver-mcp 2.0.0-beta.0 → 2.0.0-beta.10

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,8 @@ 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';
13
+ export * from './jsonMcpInstaller.js';
14
+ export * from './opencodeInstaller.js';
15
+ export * from './kiroInstaller.js';
12
16
  export * from './envFile.js';
package/dist/index.js CHANGED
@@ -9,4 +9,8 @@ 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';
13
+ export * from './jsonMcpInstaller.js';
14
+ export * from './opencodeInstaller.js';
15
+ export * from './kiroInstaller.js';
12
16
  export * from './envFile.js';
@@ -1,10 +1,11 @@
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
+ * opencode, kiro).
8
9
  * - `manual` v2 cannot mutate this runtime's config, but the path is real: it prints precise MCP/HTTP
9
10
  * wiring the operator does by hand (opencode, openclaw, mcp-generic, http-agent, server).
10
11
  * - `unsupported` v2 refuses with a clear reason (an unrecognized runtime id).
package/dist/injection.js CHANGED
@@ -1,14 +1,12 @@
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', 'opencode', 'kiro']);
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([
10
- ['opencode', 'opencode is consumed passively today; register the evolver MCP server in its config by hand'],
11
- ['kiro', 'kiro is consumed passively today; register the evolver MCP server in its config by hand'],
12
10
  ['openclaw', 'no v2 auto-installer yet; wire the evolver MCP server (or PrivateHub HTTP/A2A) by hand'],
13
11
  ['mcp-generic', 'no config writer for a generic MCP client; register the evolver MCP server by hand'],
14
12
  ['http-agent', 'no config writer for an HTTP/API-only agent; wire it to PrivateHub HTTP/A2A by hand'],
@@ -59,12 +57,25 @@ export function planInjection(runtime, server) {
59
57
  config: {},
60
58
  note: 'cursor: 渲染静默 top-gene hints 进 .cursor/rules/evolver.mdc (alwaysApply:true); daemon 在 gene 集变化时重写',
61
59
  };
62
- case 'kiro':
60
+ case 'antigravity':
61
+ return {
62
+ runtime, mode: 'mcp-config',
63
+ // Antigravity supports MCP tool discovery through its user-level JSON config. It has no verified
64
+ // SessionStart lifecycle contract, so this mode deliberately registers only the MCP server.
65
+ config: { mcpServers: { evolver: { command: server.command, args: server.args ?? [], ...(server.env ? { env: server.env } : {}) } } },
66
+ note: 'antigravity: write mcpServers.evolver in the user-level MCP config; tool discovery only (no SessionStart hook)',
67
+ };
63
68
  case 'opencode':
64
69
  return {
65
- runtime, mode: 'passive',
66
- config: {},
67
- note: `${runtime}: MVP 仅被动消费会话日志(无工具注入); 接入方式待补`,
70
+ runtime, mode: 'mcp-config',
71
+ config: { mcp: { evolver: { type: 'local', command: [server.command, ...(server.args ?? [])], ...(server.env ? { environment: server.env } : {}), enabled: true } } },
72
+ note: 'OpenCode: write opencode.json local MCP registration; no lifecycle prompt injection.',
73
+ };
74
+ case 'kiro':
75
+ return {
76
+ runtime, mode: 'mcp-config',
77
+ config: { mcpServers: { evolver: { command: server.command, args: server.args ?? [], ...(server.env ? { env: server.env } : {}), disabled: false } } },
78
+ note: 'Kiro: write .kiro/settings/mcp.json MCP registration; no lifecycle prompt injection.',
68
79
  };
69
80
  default: {
70
81
  const _exhaustive = runtime;
@@ -76,7 +87,7 @@ export function planInjection(runtime, server) {
76
87
  * 故不计入此处;passive runtime 也为 false. */
77
88
  export function injectsTools(runtime) {
78
89
  const mode = planInjection(runtime, { command: 'x' }).mode;
79
- return mode === 'mcp-hooks' || mode === 'mcp-plugin';
90
+ return mode === 'mcp-hooks' || mode === 'mcp-plugin' || mode === 'mcp-config';
80
91
  }
81
92
  /** 是否为 active 注入(任何把 gene 价值推回 runtime 的方式:MCP 工具发现 或 cursor rules 记忆注入). */
82
93
  export function isActiveInjection(runtime) {
@@ -32,11 +32,54 @@ export interface InstallOptions {
32
32
  hookCommand?: string;
33
33
  /** Reinstall even if an evolver install is already present. */
34
34
  force?: boolean;
35
+ /** Plan and validate without writing config or backup files. */
36
+ dryRun?: boolean;
35
37
  /** Cursor only: the top genes to render into .cursor/rules/evolver.mdc. The daemon refreshes these on change;
36
38
  * a one-shot `setup-hooks --runtime=cursor` install seeds the file (empty ⇒ a placeholder the daemon fills). */
37
39
  genes?: readonly CursorGene[];
38
40
  /** Cursor only: cap on genes rendered into the always-on rules body (token-tax bound). */
39
41
  maxGenes?: number;
42
+ /** Antigravity only: override the user home used to resolve ~/.gemini config roots. Intended for hermetic
43
+ * embedding/tests; normal callers omit it. configRoot and scope do not affect Antigravity's user config. */
44
+ homeDir?: string;
45
+ /** Kiro user scope only: direct replacement for ~/.kiro, matching KIRO_HOME semantics. */
46
+ kiroHome?: string;
47
+ /** OpenCode user scope only: explicit XDG_CONFIG_HOME used to resolve the global config. */
48
+ xdgConfigHome?: string;
49
+ /** OpenCode user scope only: explicit OPENCODE_CONFIG file override. */
50
+ opencodeConfig?: string;
51
+ /** OpenCode user scope only: explicit OPENCODE_CONFIG_DIR override. */
52
+ opencodeConfigDir?: string;
53
+ /** OpenCode only: inline config loaded after project/custom-directory config. */
54
+ opencodeConfigContent?: string;
55
+ /** OpenCode only: mirrors truthy OPENCODE_DISABLE_PROJECT_CONFIG handling. */
56
+ opencodeDisableProjectConfig?: boolean;
57
+ /** OpenCode only: injectable managed-config directory for hermetic tests. */
58
+ opencodeManagedConfigDir?: string;
59
+ /** OpenCode only: injectable macOS managed-preference paths for hermetic tests. */
60
+ opencodeManagedPreferencePaths?: readonly string[];
61
+ /** OpenCode only: injectable platform used to resolve system managed paths. */
62
+ opencodePlatform?: NodeJS.Platform;
63
+ /** OpenCode only: injectable ProgramData used to resolve the Windows managed path. */
64
+ opencodeProgramData?: string;
65
+ /** OpenCode only: injectable username used to resolve macOS managed preferences. */
66
+ opencodeUsername?: string;
67
+ }
68
+ export interface UninstallOptions {
69
+ configRoot: string;
70
+ scope?: InstallScope;
71
+ /** Antigravity only: override the user home used to resolve ~/.gemini config roots. */
72
+ homeDir?: string;
73
+ /** Kiro user scope only: direct replacement for ~/.kiro, matching KIRO_HOME semantics. */
74
+ kiroHome?: string;
75
+ /** Validate and report the uninstall without changing config or backup files. */
76
+ dryRun?: boolean;
77
+ /** OpenCode user scope only: explicit XDG_CONFIG_HOME used to resolve the global config. */
78
+ xdgConfigHome?: string;
79
+ /** OpenCode user scope only: explicit OPENCODE_CONFIG file override. */
80
+ opencodeConfig?: string;
81
+ /** OpenCode user scope only: explicit OPENCODE_CONFIG_DIR override. */
82
+ opencodeConfigDir?: string;
40
83
  }
41
84
  export interface InstallResult {
42
85
  ok: boolean;
@@ -45,6 +88,9 @@ export interface InstallResult {
45
88
  /** Absolute paths written (install) or cleaned (uninstall). */
46
89
  files: string[];
47
90
  alreadyInstalled?: boolean;
91
+ dryRun?: boolean;
92
+ verified?: boolean;
93
+ backups?: string[];
48
94
  error?: string;
49
95
  }
50
96
  export declare class SymlinkRefusedError extends Error {
@@ -60,14 +106,14 @@ export declare class SymlinkRefusedError extends Error {
60
106
  * .mcp.json/.claude/settings.json are evolver-owned, so their lenient fresh-start behavior stays unchanged.
61
107
  */
62
108
  export declare class UnparseableConfigError extends Error {
63
- constructor(label: string, path: string);
109
+ constructor(label: string, path: string, owner?: string);
64
110
  }
65
111
  /**
66
112
  * Thrown when a SHARED user config exists but is empty or whitespace-only. Claude Code writes these files with a
67
113
  * truncating write, so present-empty can be a concurrent-write window rather than a fresh config.
68
114
  */
69
115
  export declare class EmptySharedConfigError extends Error {
70
- constructor(label: string, path: string);
116
+ constructor(label: string, path: string, owner?: string);
71
117
  }
72
118
  type SharedConfigRaceHook = (path: string, attempt: number) => void;
73
119
  export declare function _setSharedConfigRaceHookForTest(hook?: SharedConfigRaceHook): void;
@@ -90,6 +136,8 @@ export declare function stripManaged(data: Record<string, unknown>): {
90
136
  * (delegated to codexInstaller; TOML, not JSON). Same hybrid value (tool discovery + session-start injection).
91
137
  * - cursor (cursor-rules): renders top genes into <root>/.cursor/rules/evolver.mdc (alwaysApply:true) — gene
92
138
  * memory injection, not MCP tool discovery (delegated to cursorRulesInstaller). The daemon keeps it fresh.
139
+ * - antigravity (mcp-config): writes mcpServers.evolver to every existing user-level Antigravity config root,
140
+ * or the canonical root when none exists. MCP tool discovery only; no SessionStart hook is installed.
93
141
  * Idempotent + symlink-safe; passive runtimes (kiro/opencode) return ok:false (nothing to inject).
94
142
  */
95
143
  export declare function installInjection(plan: InjectionPlan, opts: InstallOptions): InstallResult;
@@ -97,10 +145,7 @@ export declare function installInjection(plan: InjectionPlan, opts: InstallOptio
97
145
  * Pass the SAME scope used at install: 'user' cleans ~/.claude.json + ~/.claude/settings.json; 'project'
98
146
  * (default) cleans <configRoot>/.mcp.json + <configRoot>/.claude/settings.json. stripManaged only removes
99
147
  * 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;
148
+ export declare function uninstallInjection(runtime: RuntimeId, opts: UninstallOptions): InstallResult;
104
149
  /** Convenience: plan + install in one call for a runtime. */
105
150
  export declare function setupRuntime(runtime: RuntimeId, opts: InstallOptions): InstallResult;
106
151
  export {};
package/dist/installer.js CHANGED
@@ -22,6 +22,9 @@ 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';
26
+ import { installOpenCode, uninstallOpenCode } from './opencodeInstaller.js';
27
+ import { installKiro, uninstallKiro } from './kiroInstaller.js';
25
28
  /** Marks a config file as containing evolver-managed entries, so uninstall only removes what we added. */
26
29
  export const MANAGED_MARKER = '_evolver_managed';
27
30
  /** A hook entry is evolver-owned if any of its commands mention this — used to replace-not-duplicate on reinstall. */
@@ -49,8 +52,8 @@ export class SymlinkRefusedError extends Error {
49
52
  * .mcp.json/.claude/settings.json are evolver-owned, so their lenient fresh-start behavior stays unchanged.
50
53
  */
51
54
  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.`);
55
+ constructor(label, path, owner = 'Claude Code') {
56
+ 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
57
  this.name = 'UnparseableConfigError';
55
58
  }
56
59
  }
@@ -59,8 +62,8 @@ export class UnparseableConfigError extends Error {
59
62
  * truncating write, so present-empty can be a concurrent-write window rather than a fresh config.
60
63
  */
61
64
  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.`);
65
+ constructor(label, path, owner = 'Claude Code') {
66
+ 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
67
  this.name = 'EmptySharedConfigError';
65
68
  }
66
69
  }
@@ -386,6 +389,8 @@ function claudeCodeTargets(scope, configRoot) {
386
389
  * (delegated to codexInstaller; TOML, not JSON). Same hybrid value (tool discovery + session-start injection).
387
390
  * - cursor (cursor-rules): renders top genes into <root>/.cursor/rules/evolver.mdc (alwaysApply:true) — gene
388
391
  * memory injection, not MCP tool discovery (delegated to cursorRulesInstaller). The daemon keeps it fresh.
392
+ * - antigravity (mcp-config): writes mcpServers.evolver to every existing user-level Antigravity config root,
393
+ * or the canonical root when none exists. MCP tool discovery only; no SessionStart hook is installed.
389
394
  * Idempotent + symlink-safe; passive runtimes (kiro/opencode) return ok:false (nothing to inject).
390
395
  */
391
396
  export function installInjection(plan, opts) {
@@ -398,9 +403,17 @@ export function installInjection(plan, opts) {
398
403
  if (plan.runtime === 'cursor') {
399
404
  return installCursorRules({ configRoot: opts.configRoot, genes: opts.genes ?? [], ...(opts.maxGenes !== undefined ? { maxGenes: opts.maxGenes } : {}) });
400
405
  }
406
+ if (plan.runtime === 'antigravity') {
407
+ return installAntigravity(plan, opts);
408
+ }
409
+ if (plan.runtime === 'opencode') {
410
+ return installOpenCode(plan, opts);
411
+ }
412
+ if (plan.runtime === 'kiro') {
413
+ return installKiro(plan, opts);
414
+ }
401
415
  if (plan.runtime !== 'claude-code') {
402
- // 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)` };
416
+ return { ok: false, runtime: plan.runtime, mode: plan.mode, files: [], error: `installer not yet implemented for ${plan.runtime} (supported: claude-code, codex, cursor, antigravity, opencode, kiro)` };
404
417
  }
405
418
  const hookCommand = opts.hookCommand ?? DEFAULT_HOOK_COMMAND;
406
419
  const scope = opts.scope ?? 'project';
@@ -478,6 +491,15 @@ export function uninstallInjection(runtime, opts) {
478
491
  if (runtime === 'cursor') {
479
492
  return uninstallCursorRules(opts);
480
493
  }
494
+ if (runtime === 'antigravity') {
495
+ return uninstallAntigravity(runtime, opts);
496
+ }
497
+ if (runtime === 'opencode') {
498
+ return uninstallOpenCode(runtime, opts);
499
+ }
500
+ if (runtime === 'kiro') {
501
+ return uninstallKiro(runtime, opts);
502
+ }
481
503
  if (runtime !== 'claude-code') {
482
504
  return { ok: false, runtime, mode: 'n/a', files: [], error: `uninstall not implemented for ${runtime}` };
483
505
  }
@@ -0,0 +1,75 @@
1
+ import type { InjectionPlan, McpServerCmd, RuntimeId } from './injection.js';
2
+ import { type InstallOptions, type InstallResult, type UninstallOptions } from './installer.js';
3
+ type BeforeReplaceHook = (path: string) => void;
4
+ export declare function _setJsonMcpBeforeReplaceHookForTest(hook?: BeforeReplaceHook): void;
5
+ export declare function _setJsonMcpAfterReplaceHookForTest(hook?: BeforeReplaceHook): void;
6
+ export declare function _setJsonMcpBeforeBackupRemoveHookForTest(hook?: BeforeReplaceHook): void;
7
+ export interface JsonMcpRuntimeSpec {
8
+ runtime: 'opencode' | 'kiro';
9
+ configPath(opts: JsonMcpPathOptions): string;
10
+ resolveConfig?(opts: JsonMcpPathOptions): JsonMcpRuntimeResolution;
11
+ safeRoot?(opts: JsonMcpPathOptions): string;
12
+ conflictingPaths?(opts: JsonMcpPathOptions): string[];
13
+ containerKey: 'mcp' | 'mcpServers';
14
+ entry(server: McpServerCmd): Record<string, unknown>;
15
+ installPreflight?(resolution: JsonMcpRuntimeResolution, opts: InstallOptions, expected: Record<string, unknown>): JsonMcpInstallPreflight | undefined;
16
+ }
17
+ export interface JsonMcpInstallPreflight {
18
+ alreadyInstalled?: boolean;
19
+ assertUnchanged(): void;
20
+ }
21
+ type JsonMcpPathOptions = Pick<InstallOptions, 'configRoot' | 'scope' | 'homeDir' | 'kiroHome' | 'xdgConfigHome' | 'opencodeConfig' | 'opencodeConfigDir'>;
22
+ export interface JsonMcpRuntimeResolution {
23
+ configPath: string;
24
+ safeRoot: string;
25
+ conflictingPaths: string[];
26
+ /** An active sibling at the same precedence slot that must remain user-owned and read-only. */
27
+ activePrecedencePath?: string;
28
+ /** The writable target was retargeted to a uniquely managed config discovered outside the active path. */
29
+ managedRetarget?: true;
30
+ /** Runtime config candidates ordered from lowest to highest precedence; the first path is the default target. */
31
+ topologyCandidatePaths?: string[];
32
+ evidencePaths?: string[];
33
+ /** Config locations that may contain a managed backup from an earlier active-precedence decision. */
34
+ uninstallCandidatePaths?: string[];
35
+ /** Broader locations used to reuse a managed target selected from an ancestor project directory. */
36
+ installDiscoveryPaths?: string[];
37
+ /** Safe root for install discovery and a selected managed target; does not affect default install writes. */
38
+ installSafeRoot?: string;
39
+ /** Broader read-only locations used only to discover managed backups during uninstall. */
40
+ uninstallDiscoveryPaths?: string[];
41
+ /** Safe root for uninstall discovery and a selected managed target; does not affect install writes. */
42
+ uninstallSafeRoot?: string;
43
+ /** Candidate-local conflicts that must be re-evaluated after uninstall retargeting. */
44
+ uninstallConflictingPaths?: (configPath: string) => string[];
45
+ }
46
+ export declare class McpConfigConflictError extends Error {
47
+ readonly diff: {
48
+ path: string;
49
+ expected: unknown;
50
+ actual: unknown;
51
+ };
52
+ constructor(runtime: string, path: string, expected: unknown, actual: unknown);
53
+ }
54
+ export declare class McpConfigShapeError extends Error {
55
+ constructor(runtime: string, path: string, detail: string);
56
+ }
57
+ export declare class McpConfigOwnershipError extends Error {
58
+ constructor(runtime: string, detail: string);
59
+ }
60
+ export declare class McpConfigVerificationError extends Error {
61
+ private readonly runtime;
62
+ private readonly path;
63
+ restored: boolean;
64
+ constructor(runtime: string, path: string);
65
+ markRestored(): void;
66
+ }
67
+ export declare class McpConfigChangedError extends Error {
68
+ constructor(runtime: string, path: string);
69
+ }
70
+ export declare class McpServerValidationError extends Error {
71
+ constructor(message: string);
72
+ }
73
+ export declare function installJsonMcpRuntime(spec: JsonMcpRuntimeSpec, plan: InjectionPlan, opts: InstallOptions): InstallResult;
74
+ export declare function uninstallJsonMcpRuntime(spec: JsonMcpRuntimeSpec, runtime: RuntimeId, opts: UninstallOptions): InstallResult;
75
+ export {};