@evomap/evolver-mcp 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codexInstaller.d.ts +34 -0
- package/dist/codexInstaller.js +171 -0
- package/dist/cursorRulesInstaller.d.ts +76 -0
- package/dist/cursorRulesInstaller.js +196 -0
- package/dist/envFile.d.ts +10 -0
- package/dist/envFile.js +68 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/injection.d.ts +56 -0
- package/dist/injection.js +84 -0
- package/dist/installer.d.ts +106 -0
- package/dist/installer.js +513 -0
- package/dist/manualWiring.d.ts +14 -0
- package/dist/manualWiring.js +91 -0
- package/dist/primer.d.ts +12 -0
- package/dist/primer.js +32 -0
- package/dist/proxyClient.d.ts +72 -0
- package/dist/proxyClient.js +193 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +38 -0
- package/dist/serviceGuidance.d.ts +15 -0
- package/dist/serviceGuidance.js +170 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +107 -0
- package/dist/tools.d.ts +39 -0
- package/dist/tools.js +401 -0
- package/package.json +35 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RuntimeId, InjectionPlan } from './injection.js';
|
|
2
|
+
import { type InstallResult, type InstallOptions } from './installer.js';
|
|
3
|
+
/** The MCP server id evolver registers under [mcp_servers.evolver] / removes on uninstall. */
|
|
4
|
+
export declare const CODEX_MCP_SERVER_ID = "evolver";
|
|
5
|
+
/** The [mcp_servers.evolver] table from a plan's launch command. env omitted when empty (no `[..env]` table). */
|
|
6
|
+
export declare function codexMcpServerEntry(server: {
|
|
7
|
+
command: string;
|
|
8
|
+
args?: string[];
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
}): Record<string, unknown>;
|
|
11
|
+
/** A single [[hooks.SessionStart]] entry that runs `command` at session start. Shape matches codex's hooks schema. */
|
|
12
|
+
export declare function codexSessionStartHook(command: string): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Merge evolver's codex config into the user's existing parsed TOML. Idempotent + non-destructive:
|
|
15
|
+
* - [mcp_servers] : set our `evolver` server, keep every other server the user registered.
|
|
16
|
+
* - [[hooks.SessionStart]] : drop any prior evolver-owned entry, keep all user entries, append ours fresh.
|
|
17
|
+
* The user's unrelated tables (model, approval_policy, other hook events, …) pass through untouched.
|
|
18
|
+
*/
|
|
19
|
+
export declare function mergeCodexConfig(existing: Record<string, unknown>, mcpServer: Record<string, unknown>, sessionStartHook: Record<string, unknown>): Record<string, unknown>;
|
|
20
|
+
/** Strip evolver's MCP server + SessionStart hook from parsed codex TOML (uninstall). Returns [changed, data]. */
|
|
21
|
+
export declare function stripCodexManaged(data: Record<string, unknown>): {
|
|
22
|
+
changed: boolean;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Execute a codex InjectionPlan against a project config root: write/merge <root>/.codex/config.toml with
|
|
27
|
+
* the evolver MCP server registration + a SessionStart hook. Idempotent (re-running without --force is a
|
|
28
|
+
* no-op once evolver is registered) and symlink-safe.
|
|
29
|
+
*/
|
|
30
|
+
export declare function installCodex(plan: InjectionPlan, opts: InstallOptions): InstallResult;
|
|
31
|
+
/** Remove evolver's MCP registration + SessionStart hook from a codex project config (leaves user content intact). */
|
|
32
|
+
export declare function uninstallCodex(runtime: RuntimeId, opts: {
|
|
33
|
+
configRoot: string;
|
|
34
|
+
}): InstallResult;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Codex injection INSTALLER — the codex analogue of the Claude Code installer in installer.ts.
|
|
2
|
+
//
|
|
3
|
+
// Codex (the OpenAI CLI) loads its config from a TOML file, NOT JSON. User-level config lives at
|
|
4
|
+
// ~/.codex/config.toml; a project-scoped override lives at <project>/.codex/config.toml. We write the
|
|
5
|
+
// project-scoped file (same containment posture as the CC adapter, which only ever touches the project root —
|
|
6
|
+
// never a user's global home config). Codex supports the SAME injection hybrid CC does:
|
|
7
|
+
// 1. an MCP stdio server, registered under [mcp_servers.<name>] → codex discovers evolver's tools, and
|
|
8
|
+
// 2. a SessionStart lifecycle hook, registered under [[hooks.SessionStart]] → memory is pushed at session
|
|
9
|
+
// start (MCP alone can't push; the agent must pull — identical to the CC rationale).
|
|
10
|
+
// So a codex user gets the same value a CC user gets: tool discovery + session-start memory injection.
|
|
11
|
+
//
|
|
12
|
+
// Format sources (codex official docs, verified 2026-06):
|
|
13
|
+
// - [mcp_servers.<id>] { command, args=[...], [mcp_servers.<id>.env] {K=V} } (developers.openai.com/codex/mcp)
|
|
14
|
+
// - [[hooks.SessionStart]] { matcher } + [[hooks.SessionStart.hooks]] { type="command", command, ... }
|
|
15
|
+
// (developers.openai.com/codex/hooks) — only type:"command" handlers run today.
|
|
16
|
+
//
|
|
17
|
+
// Hardened exactly like the CC installer: atomic writes (tmp+rename), refusal to follow a symlink at any
|
|
18
|
+
// adapter-owned path, marker-managed so reinstall/uninstall only touch evolver's own entries, and a
|
|
19
|
+
// hooks-UNION merge that preserves the user's existing hooks. TOML round-trips through smol-toml (spec
|
|
20
|
+
// parser/serializer) so a user's hand-written config is preserved rather than clobbered.
|
|
21
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
24
|
+
import { SymlinkRefusedError, DEFAULT_HOOK_COMMAND } from './installer.js';
|
|
25
|
+
/** The MCP server id evolver registers under [mcp_servers.evolver] / removes on uninstall. */
|
|
26
|
+
export const CODEX_MCP_SERVER_ID = 'evolver';
|
|
27
|
+
/** A hook entry is evolver-owned if any command mentions this — used to replace-not-duplicate on reinstall. */
|
|
28
|
+
const EVOLVER_HOOK_TAG = 'evolver';
|
|
29
|
+
/** SessionStart matcher: codex fires `startup` on a fresh thread and `resume` on a resumed one — cover both. */
|
|
30
|
+
const SESSION_START_MATCHER = 'startup|resume';
|
|
31
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
32
|
+
// ── fs hardening (mirrors installer.ts) ──────────────────────────────────────
|
|
33
|
+
function assertNotSymlink(path, label) {
|
|
34
|
+
let st;
|
|
35
|
+
try {
|
|
36
|
+
st = lstatSync(path);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e.code === 'ENOENT')
|
|
40
|
+
return;
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
if (st.isSymbolicLink())
|
|
44
|
+
throw new SymlinkRefusedError(label, path);
|
|
45
|
+
}
|
|
46
|
+
function readToml(path) {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(path))
|
|
49
|
+
return {};
|
|
50
|
+
const raw = readFileSync(path, 'utf8').trim();
|
|
51
|
+
return raw ? parseToml(raw) : {};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {}; // unparseable → start fresh (merge re-adds evolver entries; a broken file isn't silently kept)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeTomlAtomic(path, data) {
|
|
58
|
+
const tmp = `${path}.tmp`;
|
|
59
|
+
writeFileSync(tmp, `${stringifyToml(data)}\n`, 'utf8');
|
|
60
|
+
renameSync(tmp, path);
|
|
61
|
+
}
|
|
62
|
+
// ── pure merge (exported for tests) ──────────────────────────────────────────
|
|
63
|
+
/** The [mcp_servers.evolver] table from a plan's launch command. env omitted when empty (no `[..env]` table). */
|
|
64
|
+
export function codexMcpServerEntry(server) {
|
|
65
|
+
return {
|
|
66
|
+
command: server.command,
|
|
67
|
+
args: server.args ?? [],
|
|
68
|
+
...(server.env && Object.keys(server.env).length > 0 ? { env: server.env } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A single [[hooks.SessionStart]] entry that runs `command` at session start. Shape matches codex's hooks schema. */
|
|
72
|
+
export function codexSessionStartHook(command) {
|
|
73
|
+
return { matcher: SESSION_START_MATCHER, hooks: [{ type: 'command', command }] };
|
|
74
|
+
}
|
|
75
|
+
const hookIsEvolverOwned = (entry) => {
|
|
76
|
+
if (!isObj(entry))
|
|
77
|
+
return false;
|
|
78
|
+
const inner = entry['hooks'];
|
|
79
|
+
if (!Array.isArray(inner))
|
|
80
|
+
return false;
|
|
81
|
+
return inner.some((h) => isObj(h) && typeof h['command'] === 'string' && h['command'].includes(EVOLVER_HOOK_TAG));
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Merge evolver's codex config into the user's existing parsed TOML. Idempotent + non-destructive:
|
|
85
|
+
* - [mcp_servers] : set our `evolver` server, keep every other server the user registered.
|
|
86
|
+
* - [[hooks.SessionStart]] : drop any prior evolver-owned entry, keep all user entries, append ours fresh.
|
|
87
|
+
* The user's unrelated tables (model, approval_policy, other hook events, …) pass through untouched.
|
|
88
|
+
*/
|
|
89
|
+
export function mergeCodexConfig(existing, mcpServer, sessionStartHook) {
|
|
90
|
+
const out = { ...existing };
|
|
91
|
+
const mcpServers = isObj(out['mcp_servers']) ? { ...out['mcp_servers'] } : {};
|
|
92
|
+
mcpServers[CODEX_MCP_SERVER_ID] = mcpServer;
|
|
93
|
+
out['mcp_servers'] = mcpServers;
|
|
94
|
+
const hooks = isObj(out['hooks']) ? { ...out['hooks'] } : {};
|
|
95
|
+
const prior = Array.isArray(hooks['SessionStart']) ? hooks['SessionStart'] : [];
|
|
96
|
+
hooks['SessionStart'] = [...prior.filter((e) => !hookIsEvolverOwned(e)), sessionStartHook];
|
|
97
|
+
out['hooks'] = hooks;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** Strip evolver's MCP server + SessionStart hook from parsed codex TOML (uninstall). Returns [changed, data]. */
|
|
101
|
+
export function stripCodexManaged(data) {
|
|
102
|
+
let changed = false;
|
|
103
|
+
const out = { ...data };
|
|
104
|
+
if (isObj(out['mcp_servers']) && CODEX_MCP_SERVER_ID in out['mcp_servers']) {
|
|
105
|
+
const next = { ...out['mcp_servers'] };
|
|
106
|
+
delete next[CODEX_MCP_SERVER_ID];
|
|
107
|
+
changed = true;
|
|
108
|
+
if (Object.keys(next).length > 0)
|
|
109
|
+
out['mcp_servers'] = next;
|
|
110
|
+
else
|
|
111
|
+
delete out['mcp_servers'];
|
|
112
|
+
}
|
|
113
|
+
if (isObj(out['hooks'])) {
|
|
114
|
+
const hooks = { ...out['hooks'] };
|
|
115
|
+
if (Array.isArray(hooks['SessionStart'])) {
|
|
116
|
+
const arr = hooks['SessionStart'];
|
|
117
|
+
const kept = arr.filter((e) => !hookIsEvolverOwned(e));
|
|
118
|
+
if (kept.length !== arr.length)
|
|
119
|
+
changed = true;
|
|
120
|
+
if (kept.length > 0)
|
|
121
|
+
hooks['SessionStart'] = kept;
|
|
122
|
+
else
|
|
123
|
+
delete hooks['SessionStart'];
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(hooks).length > 0)
|
|
126
|
+
out['hooks'] = hooks;
|
|
127
|
+
else
|
|
128
|
+
delete out['hooks'];
|
|
129
|
+
}
|
|
130
|
+
return { changed, data: out };
|
|
131
|
+
}
|
|
132
|
+
/** True if evolver's MCP server is already registered in this parsed config (structural install marker). */
|
|
133
|
+
function codexAlreadyInstalled(cfg) {
|
|
134
|
+
return isObj(cfg['mcp_servers']) && CODEX_MCP_SERVER_ID in cfg['mcp_servers'];
|
|
135
|
+
}
|
|
136
|
+
// ── install / uninstall ───────────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Execute a codex InjectionPlan against a project config root: write/merge <root>/.codex/config.toml with
|
|
139
|
+
* the evolver MCP server registration + a SessionStart hook. Idempotent (re-running without --force is a
|
|
140
|
+
* no-op once evolver is registered) and symlink-safe.
|
|
141
|
+
*/
|
|
142
|
+
export function installCodex(plan, opts) {
|
|
143
|
+
const hookCommand = opts.hookCommand ?? DEFAULT_HOOK_COMMAND;
|
|
144
|
+
const codexDir = join(opts.configRoot, '.codex');
|
|
145
|
+
const configPath = join(codexDir, 'config.toml');
|
|
146
|
+
assertNotSymlink(opts.configRoot, 'config root');
|
|
147
|
+
assertNotSymlink(codexDir, '.codex');
|
|
148
|
+
assertNotSymlink(configPath, '.codex/config.toml');
|
|
149
|
+
const existing = readToml(configPath);
|
|
150
|
+
if (!opts.force && codexAlreadyInstalled(existing)) {
|
|
151
|
+
return { ok: true, runtime: plan.runtime, mode: plan.mode, files: [], alreadyInstalled: true };
|
|
152
|
+
}
|
|
153
|
+
const mcpServer = codexMcpServerEntry(opts.server);
|
|
154
|
+
const sessionStartHook = codexSessionStartHook(hookCommand);
|
|
155
|
+
const merged = mergeCodexConfig(existing, mcpServer, sessionStartHook);
|
|
156
|
+
mkdirSync(codexDir, { recursive: true });
|
|
157
|
+
writeTomlAtomic(configPath, merged);
|
|
158
|
+
return { ok: true, runtime: plan.runtime, mode: plan.mode, files: [configPath] };
|
|
159
|
+
}
|
|
160
|
+
/** Remove evolver's MCP registration + SessionStart hook from a codex project config (leaves user content intact). */
|
|
161
|
+
export function uninstallCodex(runtime, opts) {
|
|
162
|
+
const configPath = join(opts.configRoot, '.codex', 'config.toml');
|
|
163
|
+
assertNotSymlink(configPath, '.codex/config.toml');
|
|
164
|
+
if (!existsSync(configPath))
|
|
165
|
+
return { ok: true, runtime, mode: 'uninstall', files: [] };
|
|
166
|
+
const { changed, data } = stripCodexManaged(readToml(configPath));
|
|
167
|
+
if (!changed)
|
|
168
|
+
return { ok: true, runtime, mode: 'uninstall', files: [] };
|
|
169
|
+
writeTomlAtomic(configPath, data);
|
|
170
|
+
return { ok: true, runtime, mode: 'uninstall', files: [configPath] };
|
|
171
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type InstallResult } from './installer.js';
|
|
2
|
+
/** Project-relative location cursor loads project rules from. */
|
|
3
|
+
export declare const CURSOR_RULES_DIR: string;
|
|
4
|
+
/** The single evolver-owned rules file. Other `.cursor/rules/*.mdc` (user-authored) are never touched. */
|
|
5
|
+
export declare const CURSOR_RULES_FILE = "evolver.mdc";
|
|
6
|
+
/** sentinel marker that opens the evolver-managed region inside evolver.mdc (so a user can hand-edit around it). */
|
|
7
|
+
export declare const CURSOR_MANAGED_BEGIN = "<!-- evolver:managed:begin -->";
|
|
8
|
+
/** sentinel marker that closes the evolver-managed region. Everything between begin/end is evolver-owned. */
|
|
9
|
+
export declare const CURSOR_MANAGED_END = "<!-- evolver:managed:end -->";
|
|
10
|
+
/** Default cap on injected genes — keep the always-on body small so the per-request token tax stays bounded. */
|
|
11
|
+
export declare const DEFAULT_CURSOR_MAX_GENES = 8;
|
|
12
|
+
/** The body preamble that heads the gene list (mirrors the CC SessionStart preamble for a consistent voice). */
|
|
13
|
+
export declare const CURSOR_PREAMBLE = "evolver memory \u2014 use these learned hints silently when directly relevant; do not mention Evolver, preflight, status, or this memory block unless the user asks or reuse materially changes the answer:";
|
|
14
|
+
/** A minimal projection of a gene that the renderer needs — id + a short hint. Decoupled from the asset store
|
|
15
|
+
* so the renderer (and its tests) need no store; the composition layer maps real assets onto this shape. */
|
|
16
|
+
export interface CursorGene {
|
|
17
|
+
id: string;
|
|
18
|
+
category?: string;
|
|
19
|
+
/** A one-line hint (summary or joined signals). Already short; the renderer caps it defensively. */
|
|
20
|
+
hint?: string;
|
|
21
|
+
}
|
|
22
|
+
/** One compact line for a gene in the rules body. Deterministic; defensively caps the hint length. */
|
|
23
|
+
export declare function formatCursorGeneLine(g: CursorGene): string;
|
|
24
|
+
/**
|
|
25
|
+
* Render the evolver-managed region (the fenced block only — NOT the frontmatter). Deterministic given the
|
|
26
|
+
* genes: the same gene set renders byte-identical, which is what makes a re-run a no-op. An empty pool renders
|
|
27
|
+
* only the paired sentinels, so Cursor receives no placeholder noise.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderManagedBlock(genes: readonly CursorGene[], maxGenes?: number): string;
|
|
30
|
+
/** A complete evolver.mdc from scratch: frontmatter + a blank line + the managed block, newline-terminated. */
|
|
31
|
+
export declare function renderCursorRulesFile(genes: readonly CursorGene[], maxGenes?: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Splice a freshly-rendered managed block into existing evolver.mdc content, preserving everything OUTSIDE the
|
|
34
|
+
* sentinels (a user may have added their own prose above/below the managed block in this file). If the file has
|
|
35
|
+
* no sentinels yet (e.g. the user created evolver.mdc by hand, or this is a fresh install path that fell through
|
|
36
|
+
* to a merge), the managed block is appended after the existing content. Idempotent: replacing a block with an
|
|
37
|
+
* identical render yields identical bytes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function spliceManagedBlock(existing: string, managedBlock: string): string;
|
|
40
|
+
/** Remove the evolver-managed region (and the surrounding blank line we may have inserted) from evolver.mdc
|
|
41
|
+
* content, leaving any user-authored prose intact. Returns [changed, text]. */
|
|
42
|
+
export declare function stripManagedBlock(existing: string): {
|
|
43
|
+
changed: boolean;
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
/** Where evolver.mdc lives under a config root. */
|
|
47
|
+
export declare function cursorRulesPath(configRoot: string): string;
|
|
48
|
+
export interface CursorInstallOptions {
|
|
49
|
+
configRoot: string;
|
|
50
|
+
genes: readonly CursorGene[];
|
|
51
|
+
maxGenes?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Write/refresh `.cursor/rules/evolver.mdc` from the current top genes. Non-destructive + idempotent:
|
|
55
|
+
* - a fresh install writes the full file (frontmatter + managed block);
|
|
56
|
+
* - a re-render only replaces the managed block, preserving frontmatter and any user prose in the same file;
|
|
57
|
+
* - re-running with an unchanged gene set produces identical bytes ⇒ we skip the write (a true no-op, so
|
|
58
|
+
* mtime/inode are untouched and nothing downstream sees a spurious change).
|
|
59
|
+
* Symlink-hardened on every adapter-owned path; other `.cursor/rules/*.mdc` files are never read or written.
|
|
60
|
+
*/
|
|
61
|
+
export declare function installCursorRules(opts: CursorInstallOptions): InstallResult & {
|
|
62
|
+
rewritten: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Remove evolver's cursor injection. If evolver.mdc is evolver-only (we created it, no user prose), delete the
|
|
66
|
+
* file; otherwise strip just the managed block and keep the user's content. Other rules files are untouched.
|
|
67
|
+
*/
|
|
68
|
+
export declare function uninstallCursorRules(opts: {
|
|
69
|
+
configRoot: string;
|
|
70
|
+
}): InstallResult;
|
|
71
|
+
/** Convenience for the daemon rewrite trigger: rewrite the rules file to reflect the current top genes. Returns
|
|
72
|
+
* whether the file actually changed (so callers can avoid redundant downstream work / emissions). */
|
|
73
|
+
export declare function rewriteCursorRules(configRoot: string, genes: readonly CursorGene[], maxGenes?: number): boolean;
|
|
74
|
+
/** Whether cursor's injection file is currently installed at a config root (used to gate rewrite-on-change so we
|
|
75
|
+
* only maintain the file for users who opted in via `evolver setup-hooks --runtime=cursor`). */
|
|
76
|
+
export declare function cursorRulesInstalled(configRoot: string): boolean;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Cursor injection RENDERER — the cursor analogue of the Claude Code / codex installers (installer.ts /
|
|
2
|
+
// codexInstaller.ts), but a fundamentally different mechanism. Cursor has NO MCP-server-config + SessionStart
|
|
3
|
+
// lifecycle-hook hybrid to write, so the config-writer pattern does not transfer. Cursor's stable injection
|
|
4
|
+
// point is a PROJECT RULES file: it loads `.cursor/rules/*.mdc` (Markdown with YAML frontmatter), and a rule
|
|
5
|
+
// with `alwaysApply: true` is injected into the context of every request. The active path keeps that functional
|
|
6
|
+
// injection, but renders only quiet, relevant memory hints and no placeholder text for an empty gene pool.
|
|
7
|
+
//
|
|
8
|
+
// Format sources (cursor official docs, verified 2026-06 — cursor.com/docs/context/rules):
|
|
9
|
+
// - Project rules live in `.cursor/rules/` as `.mdc` files, version-controlled.
|
|
10
|
+
// - YAML frontmatter controls behavior: `alwaysApply` (boolean), `description` (string), `globs` (string).
|
|
11
|
+
// - `alwaysApply: true` ⇒ "Always included. Globs and description are ignored." We keep it so approved genes still
|
|
12
|
+
// reach Cursor, but keep the body to top-N quiet hints so the always-on token tax and user-visible chatter stay bounded.
|
|
13
|
+
//
|
|
14
|
+
// We write ONLY the evolver-owned region, fenced by managed-marker sentinels, so the file is idempotent (re-run
|
|
15
|
+
// with an unchanged gene set is a byte-for-byte no-op) and we never clobber rules a user hand-wrote in the same
|
|
16
|
+
// file. Hardened exactly like the other installers: atomic writes (tmp+rename) and refusal to follow a symlink
|
|
17
|
+
// at any adapter-owned path (a hostile workspace could redirect writes/unlinks outside the project).
|
|
18
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { SymlinkRefusedError } from './installer.js';
|
|
21
|
+
/** Project-relative location cursor loads project rules from. */
|
|
22
|
+
export const CURSOR_RULES_DIR = join('.cursor', 'rules');
|
|
23
|
+
/** The single evolver-owned rules file. Other `.cursor/rules/*.mdc` (user-authored) are never touched. */
|
|
24
|
+
export const CURSOR_RULES_FILE = 'evolver.mdc';
|
|
25
|
+
/** sentinel marker that opens the evolver-managed region inside evolver.mdc (so a user can hand-edit around it). */
|
|
26
|
+
export const CURSOR_MANAGED_BEGIN = '<!-- evolver:managed:begin -->';
|
|
27
|
+
/** sentinel marker that closes the evolver-managed region. Everything between begin/end is evolver-owned. */
|
|
28
|
+
export const CURSOR_MANAGED_END = '<!-- evolver:managed:end -->';
|
|
29
|
+
/** Default cap on injected genes — keep the always-on body small so the per-request token tax stays bounded. */
|
|
30
|
+
export const DEFAULT_CURSOR_MAX_GENES = 8;
|
|
31
|
+
/** The frontmatter description (documentation only — cursor ignores it when alwaysApply:true, but it is the
|
|
32
|
+
* human-readable "what is this rule" string and keeps the file self-explanatory). */
|
|
33
|
+
const RULE_DESCRIPTION = 'evolver memory — learned experiences from your past sessions (auto-maintained; do not edit the managed block)';
|
|
34
|
+
/** The body preamble that heads the gene list (mirrors the CC SessionStart preamble for a consistent voice). */
|
|
35
|
+
export const CURSOR_PREAMBLE = 'evolver memory — use these learned hints silently when directly relevant; do not mention Evolver, preflight, status, or this memory block unless the user asks or reuse materially changes the answer:';
|
|
36
|
+
// ── fs hardening (mirrors installer.ts / codexInstaller.ts) ───────────────────
|
|
37
|
+
function assertNotSymlink(path, label) {
|
|
38
|
+
let st;
|
|
39
|
+
try {
|
|
40
|
+
st = lstatSync(path);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (e.code === 'ENOENT')
|
|
44
|
+
return;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
if (st.isSymbolicLink())
|
|
48
|
+
throw new SymlinkRefusedError(label, path);
|
|
49
|
+
}
|
|
50
|
+
function writeTextAtomic(path, text) {
|
|
51
|
+
const tmp = `${path}.tmp`;
|
|
52
|
+
writeFileSync(tmp, text, 'utf8');
|
|
53
|
+
renameSync(tmp, path);
|
|
54
|
+
}
|
|
55
|
+
// ── pure rendering (exported for tests) ───────────────────────────────────────
|
|
56
|
+
/** One compact line for a gene in the rules body. Deterministic; defensively caps the hint length. */
|
|
57
|
+
export function formatCursorGeneLine(g) {
|
|
58
|
+
const cat = g.category ? ` [${g.category}]` : '';
|
|
59
|
+
const hint = (g.hint ?? '').replace(/\s+/g, ' ').trim();
|
|
60
|
+
return `- ${g.id}${cat}${hint ? `: ${hint.slice(0, 160)}` : ''}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render the evolver-managed region (the fenced block only — NOT the frontmatter). Deterministic given the
|
|
64
|
+
* genes: the same gene set renders byte-identical, which is what makes a re-run a no-op. An empty pool renders
|
|
65
|
+
* only the paired sentinels, so Cursor receives no placeholder noise.
|
|
66
|
+
*/
|
|
67
|
+
export function renderManagedBlock(genes, maxGenes = DEFAULT_CURSOR_MAX_GENES) {
|
|
68
|
+
const lines = genes.slice(0, maxGenes).map(formatCursorGeneLine);
|
|
69
|
+
if (lines.length === 0)
|
|
70
|
+
return `${CURSOR_MANAGED_BEGIN}\n${CURSOR_MANAGED_END}`;
|
|
71
|
+
return `${CURSOR_MANAGED_BEGIN}\n${CURSOR_PREAMBLE}\n\n${lines.join('\n')}\n${CURSOR_MANAGED_END}`;
|
|
72
|
+
}
|
|
73
|
+
/** The YAML frontmatter block. `alwaysApply: true` ⇒ injected into every request (globs/description ignored,
|
|
74
|
+
* per cursor docs, but description is kept as the human-readable label). */
|
|
75
|
+
function frontmatter() {
|
|
76
|
+
return `---\ndescription: ${RULE_DESCRIPTION}\nglobs:\nalwaysApply: true\n---`;
|
|
77
|
+
}
|
|
78
|
+
/** A complete evolver.mdc from scratch: frontmatter + a blank line + the managed block, newline-terminated. */
|
|
79
|
+
export function renderCursorRulesFile(genes, maxGenes = DEFAULT_CURSOR_MAX_GENES) {
|
|
80
|
+
return `${frontmatter()}\n\n${renderManagedBlock(genes, maxGenes)}\n`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Splice a freshly-rendered managed block into existing evolver.mdc content, preserving everything OUTSIDE the
|
|
84
|
+
* sentinels (a user may have added their own prose above/below the managed block in this file). If the file has
|
|
85
|
+
* no sentinels yet (e.g. the user created evolver.mdc by hand, or this is a fresh install path that fell through
|
|
86
|
+
* to a merge), the managed block is appended after the existing content. Idempotent: replacing a block with an
|
|
87
|
+
* identical render yields identical bytes.
|
|
88
|
+
*/
|
|
89
|
+
export function spliceManagedBlock(existing, managedBlock) {
|
|
90
|
+
const begin = existing.indexOf(CURSOR_MANAGED_BEGIN);
|
|
91
|
+
const end = existing.indexOf(CURSOR_MANAGED_END);
|
|
92
|
+
if (begin >= 0 && end > begin) {
|
|
93
|
+
const before = existing.slice(0, begin);
|
|
94
|
+
const after = existing.slice(end + CURSOR_MANAGED_END.length);
|
|
95
|
+
return `${before}${managedBlock}${after}`;
|
|
96
|
+
}
|
|
97
|
+
// No managed region yet: append, keeping the user's existing content intact (separated by a blank line).
|
|
98
|
+
const sep = existing.length === 0 ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
|
|
99
|
+
return `${existing}${sep}${managedBlock}\n`;
|
|
100
|
+
}
|
|
101
|
+
/** Remove the evolver-managed region (and the surrounding blank line we may have inserted) from evolver.mdc
|
|
102
|
+
* content, leaving any user-authored prose intact. Returns [changed, text]. */
|
|
103
|
+
export function stripManagedBlock(existing) {
|
|
104
|
+
const begin = existing.indexOf(CURSOR_MANAGED_BEGIN);
|
|
105
|
+
const end = existing.indexOf(CURSOR_MANAGED_END);
|
|
106
|
+
if (begin < 0 || end <= begin)
|
|
107
|
+
return { changed: false, text: existing };
|
|
108
|
+
const before = existing.slice(0, begin).replace(/\n+$/, '');
|
|
109
|
+
const after = existing.slice(end + CURSOR_MANAGED_END.length).replace(/^\n+/, '');
|
|
110
|
+
const joined = before && after ? `${before}\n\n${after}` : `${before}${after}`;
|
|
111
|
+
return { changed: true, text: joined };
|
|
112
|
+
}
|
|
113
|
+
/** Is this evolver.mdc content effectively "evolver-only" — i.e. nothing but our managed block and frontmatter
|
|
114
|
+
* remains once the managed region is stripped? Used to decide delete-file vs strip-block on uninstall. */
|
|
115
|
+
function isEvolverOnly(text) {
|
|
116
|
+
const { text: stripped } = stripManagedBlock(text);
|
|
117
|
+
// After stripping, anything left that is not blank / not the frontmatter we wrote ⇒ the user owns this file.
|
|
118
|
+
const residue = stripped
|
|
119
|
+
.replace(/^---[\s\S]*?---/, '') // our frontmatter block
|
|
120
|
+
.replace(/\s+/g, '')
|
|
121
|
+
.trim();
|
|
122
|
+
return residue.length === 0;
|
|
123
|
+
}
|
|
124
|
+
// ── install / uninstall / rewrite ─────────────────────────────────────────────
|
|
125
|
+
/** Where evolver.mdc lives under a config root. */
|
|
126
|
+
export function cursorRulesPath(configRoot) {
|
|
127
|
+
return join(configRoot, CURSOR_RULES_DIR, CURSOR_RULES_FILE);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Write/refresh `.cursor/rules/evolver.mdc` from the current top genes. Non-destructive + idempotent:
|
|
131
|
+
* - a fresh install writes the full file (frontmatter + managed block);
|
|
132
|
+
* - a re-render only replaces the managed block, preserving frontmatter and any user prose in the same file;
|
|
133
|
+
* - re-running with an unchanged gene set produces identical bytes ⇒ we skip the write (a true no-op, so
|
|
134
|
+
* mtime/inode are untouched and nothing downstream sees a spurious change).
|
|
135
|
+
* Symlink-hardened on every adapter-owned path; other `.cursor/rules/*.mdc` files are never read or written.
|
|
136
|
+
*/
|
|
137
|
+
export function installCursorRules(opts) {
|
|
138
|
+
const rulesDir = join(opts.configRoot, CURSOR_RULES_DIR);
|
|
139
|
+
const cursorDir = join(opts.configRoot, '.cursor');
|
|
140
|
+
const filePath = cursorRulesPath(opts.configRoot);
|
|
141
|
+
const maxGenes = opts.maxGenes ?? DEFAULT_CURSOR_MAX_GENES;
|
|
142
|
+
assertNotSymlink(opts.configRoot, 'config root');
|
|
143
|
+
assertNotSymlink(cursorDir, '.cursor');
|
|
144
|
+
assertNotSymlink(rulesDir, '.cursor/rules');
|
|
145
|
+
assertNotSymlink(filePath, '.cursor/rules/evolver.mdc');
|
|
146
|
+
const block = renderManagedBlock(opts.genes, maxGenes);
|
|
147
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
148
|
+
const next = existing ? spliceManagedBlock(existing, block) : renderCursorRulesFile(opts.genes, maxGenes);
|
|
149
|
+
// Idempotent: identical bytes ⇒ no write at all (no-op keeps the file's mtime/inode and avoids waking watchers).
|
|
150
|
+
if (existing === next) {
|
|
151
|
+
return { ok: true, runtime: 'cursor', mode: 'cursor-rules', files: [], rewritten: false };
|
|
152
|
+
}
|
|
153
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
154
|
+
writeTextAtomic(filePath, next);
|
|
155
|
+
return { ok: true, runtime: 'cursor', mode: 'cursor-rules', files: [filePath], rewritten: true };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove evolver's cursor injection. If evolver.mdc is evolver-only (we created it, no user prose), delete the
|
|
159
|
+
* file; otherwise strip just the managed block and keep the user's content. Other rules files are untouched.
|
|
160
|
+
*/
|
|
161
|
+
export function uninstallCursorRules(opts) {
|
|
162
|
+
const filePath = cursorRulesPath(opts.configRoot);
|
|
163
|
+
assertNotSymlink(filePath, '.cursor/rules/evolver.mdc');
|
|
164
|
+
if (!existsSync(filePath))
|
|
165
|
+
return { ok: true, runtime: 'cursor', mode: 'uninstall', files: [] };
|
|
166
|
+
const text = readFileSync(filePath, 'utf8');
|
|
167
|
+
if (isEvolverOnly(text)) {
|
|
168
|
+
unlinkSync(filePath);
|
|
169
|
+
return { ok: true, runtime: 'cursor', mode: 'uninstall', files: [filePath] };
|
|
170
|
+
}
|
|
171
|
+
const { changed, text: stripped } = stripManagedBlock(text);
|
|
172
|
+
if (!changed)
|
|
173
|
+
return { ok: true, runtime: 'cursor', mode: 'uninstall', files: [] };
|
|
174
|
+
writeTextAtomic(filePath, stripped.endsWith('\n') ? stripped : `${stripped}\n`);
|
|
175
|
+
return { ok: true, runtime: 'cursor', mode: 'uninstall', files: [filePath] };
|
|
176
|
+
}
|
|
177
|
+
/** Convenience for the daemon rewrite trigger: rewrite the rules file to reflect the current top genes. Returns
|
|
178
|
+
* whether the file actually changed (so callers can avoid redundant downstream work / emissions). */
|
|
179
|
+
export function rewriteCursorRules(configRoot, genes, maxGenes) {
|
|
180
|
+
return installCursorRules({ configRoot, genes, ...(maxGenes !== undefined ? { maxGenes } : {}) }).rewritten;
|
|
181
|
+
}
|
|
182
|
+
/** Whether cursor's injection file is currently installed at a config root (used to gate rewrite-on-change so we
|
|
183
|
+
* only maintain the file for users who opted in via `evolver setup-hooks --runtime=cursor`). */
|
|
184
|
+
export function cursorRulesInstalled(configRoot) {
|
|
185
|
+
const filePath = cursorRulesPath(configRoot);
|
|
186
|
+
try {
|
|
187
|
+
if (lstatSync(filePath).isSymbolicLink())
|
|
188
|
+
return false; // a symlink at our path is not a valid install
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
if (e.code === 'ENOENT')
|
|
192
|
+
return false;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return readFileSync(filePath, 'utf8').includes(CURSOR_MANAGED_BEGIN);
|
|
196
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface EnvFileLoadResult {
|
|
2
|
+
loaded: boolean;
|
|
3
|
+
path?: string;
|
|
4
|
+
keys: string[];
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function expandHomePath(path: string): string;
|
|
8
|
+
export declare function parseEnvFile(raw: string): Record<string, string>;
|
|
9
|
+
export declare function loadEnvFile(path: string, env?: Record<string, string | undefined>): EnvFileLoadResult;
|
|
10
|
+
export declare function loadEnvFileFromEnv(env?: Record<string, string | undefined>): EnvFileLoadResult;
|
package/dist/envFile.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
const ENV_FILE_KEY = 'EVOLVER_ENV_FILE';
|
|
5
|
+
export function expandHomePath(path) {
|
|
6
|
+
if (path === '~')
|
|
7
|
+
return homedir();
|
|
8
|
+
if (path.startsWith('~/'))
|
|
9
|
+
return join(homedir(), path.slice(2));
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
export function parseEnvFile(raw) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
17
|
+
continue;
|
|
18
|
+
const body = trimmed.startsWith('export ') ? trimmed.slice('export '.length).trimStart() : trimmed;
|
|
19
|
+
const eq = body.indexOf('=');
|
|
20
|
+
if (eq <= 0)
|
|
21
|
+
continue;
|
|
22
|
+
const key = body.slice(0, eq).trim();
|
|
23
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
24
|
+
continue;
|
|
25
|
+
out[key] = unquoteEnvValue(body.slice(eq + 1).trim());
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
export function loadEnvFile(path, env = process.env) {
|
|
30
|
+
const resolved = isAbsolute(path) ? path : expandHomePath(path);
|
|
31
|
+
try {
|
|
32
|
+
const parsed = parseEnvFile(readFileSync(resolved, 'utf8'));
|
|
33
|
+
const keys = Object.keys(parsed);
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
if (key === ENV_FILE_KEY)
|
|
36
|
+
continue;
|
|
37
|
+
env[key] = parsed[key];
|
|
38
|
+
}
|
|
39
|
+
return { loaded: true, path: resolved, keys };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return { loaded: false, path: resolved, keys: [], error: err instanceof Error ? err.message : String(err) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function loadEnvFileFromEnv(env = process.env) {
|
|
46
|
+
const path = env[ENV_FILE_KEY]?.trim();
|
|
47
|
+
if (!path)
|
|
48
|
+
return { loaded: false, keys: [] };
|
|
49
|
+
return loadEnvFile(path, env);
|
|
50
|
+
}
|
|
51
|
+
function unquoteEnvValue(value) {
|
|
52
|
+
if (value.length >= 2) {
|
|
53
|
+
const first = value[0];
|
|
54
|
+
const last = value[value.length - 1];
|
|
55
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
56
|
+
const inner = value.slice(1, -1);
|
|
57
|
+
return first === '"' ? inner.replace(/\\(["\\nrt])/g, (_, ch) => {
|
|
58
|
+
switch (ch) {
|
|
59
|
+
case 'n': return '\n';
|
|
60
|
+
case 'r': return '\r';
|
|
61
|
+
case 't': return '\t';
|
|
62
|
+
default: return ch;
|
|
63
|
+
}
|
|
64
|
+
}) : inner;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const PACKAGE = "@evomap/evolver-mcp";
|
|
2
|
+
export * from './tools.js';
|
|
3
|
+
export * from './primer.js';
|
|
4
|
+
export * from './proxyClient.js';
|
|
5
|
+
export * from './server.js';
|
|
6
|
+
export * from './injection.js';
|
|
7
|
+
export * from './installer.js';
|
|
8
|
+
export * from './manualWiring.js';
|
|
9
|
+
export * from './serviceGuidance.js';
|
|
10
|
+
export * from './codexInstaller.js';
|
|
11
|
+
export * from './cursorRulesInstaller.js';
|
|
12
|
+
export * from './envFile.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const PACKAGE = '@evomap/evolver-mcp';
|
|
2
|
+
export * from './tools.js';
|
|
3
|
+
export * from './primer.js';
|
|
4
|
+
export * from './proxyClient.js';
|
|
5
|
+
export * from './server.js';
|
|
6
|
+
export * from './injection.js';
|
|
7
|
+
export * from './installer.js';
|
|
8
|
+
export * from './manualWiring.js';
|
|
9
|
+
export * from './serviceGuidance.js';
|
|
10
|
+
export * from './codexInstaller.js';
|
|
11
|
+
export * from './cursorRulesInstaller.js';
|
|
12
|
+
export * from './envFile.js';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type RuntimeId = 'claude-code' | 'codex' | 'cursor' | 'kiro' | 'opencode';
|
|
2
|
+
export type InjectionMode = 'mcp-hooks' | 'mcp-plugin' | 'cursor-rules' | 'passive';
|
|
3
|
+
/**
|
|
4
|
+
* Setup support contract (#217). A bootstrapper that delegates runtime onboarding to evolver v2 needs a
|
|
5
|
+
* DETERMINISTIC answer for every runtime it might ask about — not just the ones v2 can write config for.
|
|
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).
|
|
8
|
+
* - `manual` v2 cannot mutate this runtime's config, but the path is real: it prints precise MCP/HTTP
|
|
9
|
+
* wiring the operator does by hand (opencode, openclaw, mcp-generic, http-agent, server).
|
|
10
|
+
* - `unsupported` v2 refuses with a clear reason (an unrecognized runtime id).
|
|
11
|
+
* This split is what stops the bootstrapper from reporting a runtime as installed when it is not (#217 AC).
|
|
12
|
+
*/
|
|
13
|
+
export type SetupOutcome = 'installed' | 'manual' | 'unsupported';
|
|
14
|
+
/**
|
|
15
|
+
* The runtimes a bootstrapper may ask `evolver setup-hooks` about (#217). A superset of RuntimeId: the extra
|
|
16
|
+
* ids (openclaw, mcp-generic, http-agent, server) have no auto-installer, so they live ONLY in this setup-level
|
|
17
|
+
* type and never reach `planInjection` (whose RuntimeId switch stays exhaustive over the runtimes v2 can inject).
|
|
18
|
+
*/
|
|
19
|
+
export type SetupRuntime = RuntimeId | 'openclaw' | 'mcp-generic' | 'http-agent' | 'server';
|
|
20
|
+
/** The declared support class for a runtime (the static matrix entry), plus a reason for manual/unsupported. */
|
|
21
|
+
export interface RuntimeSupport {
|
|
22
|
+
/** The runtime id as asked (echoed back so a caller parsing --json sees what it requested). */
|
|
23
|
+
runtime: string;
|
|
24
|
+
outcome: SetupOutcome;
|
|
25
|
+
/** Human reason — present for `manual` (what to do by hand) and `unsupported` (why refused); absent for installed. */
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Every runtime in the setup matrix (#217), in a stable order — used for usage text and the unsupported reason. */
|
|
29
|
+
export declare const SETUP_RUNTIMES: readonly SetupRuntime[];
|
|
30
|
+
/**
|
|
31
|
+
* Classify a runtime id into the setup matrix (#217). Takes a RAW string (not the SetupRuntime union) so the
|
|
32
|
+
* unsupported branch is reachable: an unrecognized id is the `unsupported` case, with a reason that lists the
|
|
33
|
+
* runtimes v2 does recognize. This is the single source of truth the CLI uses to decide install vs print vs refuse.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runtimeSupport(runtime: string): RuntimeSupport;
|
|
36
|
+
export interface InjectionPlan {
|
|
37
|
+
runtime: RuntimeId;
|
|
38
|
+
mode: InjectionMode;
|
|
39
|
+
config: Record<string, unknown>;
|
|
40
|
+
note: string;
|
|
41
|
+
}
|
|
42
|
+
export interface McpServerCmd {
|
|
43
|
+
command: string;
|
|
44
|
+
args?: string[];
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 按 runtime 规划 MCP 工具注入(M5-5). MVP: CC(hooks)+codex(plugin) 做工具注入,
|
|
49
|
+
* 其余仅被动会话日志消费(待确认 a / 批注#39). 注入方式差异大, 故每 runtime 一策略.
|
|
50
|
+
*/
|
|
51
|
+
export declare function planInjection(runtime: RuntimeId, server: McpServerCmd): InjectionPlan;
|
|
52
|
+
/** 哪些 runtime 经 MCP server 注入工具发现(CC+codex). cursor 注入的是 gene 记忆(rules 文件)而非 MCP 工具,
|
|
53
|
+
* 故不计入此处;passive runtime 也为 false. */
|
|
54
|
+
export declare function injectsTools(runtime: RuntimeId): boolean;
|
|
55
|
+
/** 是否为 active 注入(任何把 gene 价值推回 runtime 的方式:MCP 工具发现 或 cursor rules 记忆注入). */
|
|
56
|
+
export declare function isActiveInjection(runtime: RuntimeId): boolean;
|