@evomap/evolver-mcp 2.0.0-beta.2 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/antigravityInstaller.d.ts +1 -1
- package/dist/antigravityInstaller.js +17 -29
- package/dist/codexInstaller.d.ts +8 -3
- package/dist/codexInstaller.js +217 -53
- package/dist/cursorRulesInstaller.d.ts +1 -1
- package/dist/cursorRulesInstaller.js +66 -17
- package/dist/envFile.d.ts +2 -1
- package/dist/envFile.js +6 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/injection.d.ts +2 -1
- package/dist/injection.js +10 -7
- package/dist/installer.d.ts +52 -24
- package/dist/installer.js +268 -157
- package/dist/installerShared.d.ts +103 -0
- package/dist/installerShared.js +99 -0
- package/dist/jsonMcpInstaller.d.ts +79 -0
- package/dist/jsonMcpInstaller.js +857 -0
- package/dist/kiroInstaller.d.ts +10 -0
- package/dist/kiroInstaller.js +146 -0
- package/dist/opencodeInstaller.d.ts +18 -0
- package/dist/opencodeInstaller.js +531 -0
- package/dist/primer.js +8 -5
- package/dist/productBridge.d.ts +37 -0
- package/dist/productBridge.js +250 -0
- package/dist/productBridgeShim.d.ts +52 -0
- package/dist/productBridgeShim.js +338 -0
- package/dist/proxyClient.d.ts +27 -0
- package/dist/proxyClient.js +135 -26
- package/dist/sharedFileCommit.d.ts +20 -0
- package/dist/sharedFileCommit.js +256 -0
- package/dist/stdio.js +10 -4
- package/dist/tools.js +71 -7
- package/package.json +13 -2
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir, userInfo } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from 'node:path';
|
|
5
|
+
import { installJsonMcpRuntime, McpConfigChangedError, McpConfigConflictError, McpConfigOwnershipError, McpConfigShapeError, uninstallJsonMcpRuntime, } from './jsonMcpInstaller.js';
|
|
6
|
+
function configuredPath(value) {
|
|
7
|
+
const trimmed = value?.trim();
|
|
8
|
+
return trimmed ? (isAbsolute(trimmed) ? trimmed : resolve(trimmed)) : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function resolveOpenCodeManagedConfigDir(opts = {}) {
|
|
11
|
+
const platform = opts.opencodePlatform ?? process.platform;
|
|
12
|
+
if (platform === 'darwin')
|
|
13
|
+
return '/Library/Application Support/opencode';
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
return win32.join(opts.opencodeProgramData ?? process.env['ProgramData'] ?? 'C:\\ProgramData', 'opencode');
|
|
16
|
+
}
|
|
17
|
+
return '/etc/opencode';
|
|
18
|
+
}
|
|
19
|
+
export function resolveOpenCodeManagedPreferencePaths(opts = {}) {
|
|
20
|
+
if ((opts.opencodePlatform ?? process.platform) !== 'darwin')
|
|
21
|
+
return [];
|
|
22
|
+
let username = opts.opencodeUsername;
|
|
23
|
+
if (username === undefined) {
|
|
24
|
+
try {
|
|
25
|
+
username = userInfo().username || 'user';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
username = 'user';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
posix.join('/Library/Managed Preferences', username, 'ai.opencode.managed.plist'),
|
|
33
|
+
posix.join('/Library/Managed Preferences', 'ai.opencode.managed.plist'),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function valuesEqual(left, right) {
|
|
40
|
+
if (Object.is(left, right))
|
|
41
|
+
return true;
|
|
42
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
43
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
44
|
+
&& left.length === right.length
|
|
45
|
+
&& left.every((value, index) => valuesEqual(value, right[index]));
|
|
46
|
+
}
|
|
47
|
+
if (!isRecord(left) || !isRecord(right))
|
|
48
|
+
return false;
|
|
49
|
+
const leftKeys = Object.keys(left).sort();
|
|
50
|
+
const rightKeys = Object.keys(right).sort();
|
|
51
|
+
return leftKeys.length === rightKeys.length
|
|
52
|
+
&& leftKeys.every((key, index) => key === rightKeys[index] && valuesEqual(left[key], right[key]));
|
|
53
|
+
}
|
|
54
|
+
function mergeOpenCodeValue(target, source) {
|
|
55
|
+
if (!isRecord(target) || !isRecord(source))
|
|
56
|
+
return source;
|
|
57
|
+
const merged = { ...target };
|
|
58
|
+
for (const [key, value] of Object.entries(source)) {
|
|
59
|
+
// Define user-controlled keys as data so `__proto__` cannot invoke the legacy object setter.
|
|
60
|
+
Object.defineProperty(merged, key, {
|
|
61
|
+
configurable: true,
|
|
62
|
+
enumerable: true,
|
|
63
|
+
value: mergeOpenCodeValue(merged[key], value),
|
|
64
|
+
writable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
function mergeEffectiveEntry(current, entry, source) {
|
|
70
|
+
if (!entry.present)
|
|
71
|
+
return current;
|
|
72
|
+
return {
|
|
73
|
+
present: true,
|
|
74
|
+
value: current.present ? mergeOpenCodeValue(current.value, entry.value) : entry.value,
|
|
75
|
+
source,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function readReadOnlySource(path) {
|
|
79
|
+
if (!existsSync(path))
|
|
80
|
+
return { path, raw: null };
|
|
81
|
+
const stat = lstatSync(path);
|
|
82
|
+
if (stat.isSymbolicLink()) {
|
|
83
|
+
throw new McpConfigOwnershipError('opencode', 'a layered configuration is a symbolic link');
|
|
84
|
+
}
|
|
85
|
+
if (!stat.isFile()) {
|
|
86
|
+
throw new McpConfigOwnershipError('opencode', 'a layered configuration is not a regular file');
|
|
87
|
+
}
|
|
88
|
+
return { path, raw: readFileSync(path, 'utf8') };
|
|
89
|
+
}
|
|
90
|
+
function parseReadOnlyJsonc(raw, source) {
|
|
91
|
+
let withoutComments = '';
|
|
92
|
+
let inString = false;
|
|
93
|
+
let escaped = false;
|
|
94
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
95
|
+
const char = raw[index];
|
|
96
|
+
const next = raw[index + 1];
|
|
97
|
+
if (inString) {
|
|
98
|
+
withoutComments += char;
|
|
99
|
+
if (escaped)
|
|
100
|
+
escaped = false;
|
|
101
|
+
else if (char === '\\')
|
|
102
|
+
escaped = true;
|
|
103
|
+
else if (char === '"')
|
|
104
|
+
inString = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '"') {
|
|
108
|
+
inString = true;
|
|
109
|
+
withoutComments += char;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (char === '/' && next === '/') {
|
|
113
|
+
withoutComments += ' ';
|
|
114
|
+
index += 1;
|
|
115
|
+
while (index + 1 < raw.length && raw[index + 1] !== '\n' && raw[index + 1] !== '\r') {
|
|
116
|
+
withoutComments += ' ';
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === '/' && next === '*') {
|
|
122
|
+
withoutComments += ' ';
|
|
123
|
+
index += 1;
|
|
124
|
+
let closed = false;
|
|
125
|
+
while (index + 1 < raw.length) {
|
|
126
|
+
const current = raw[index + 1];
|
|
127
|
+
const following = raw[index + 2];
|
|
128
|
+
if (current === '*' && following === '/') {
|
|
129
|
+
withoutComments += ' ';
|
|
130
|
+
index += 2;
|
|
131
|
+
closed = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
withoutComments += current === '\n' || current === '\r' ? current : ' ';
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
if (!closed)
|
|
138
|
+
throw new McpConfigShapeError('opencode', source, 'layered JSONC configuration has an unterminated comment');
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
withoutComments += char;
|
|
142
|
+
}
|
|
143
|
+
let normalized = '';
|
|
144
|
+
inString = false;
|
|
145
|
+
escaped = false;
|
|
146
|
+
for (let index = 0; index < withoutComments.length; index += 1) {
|
|
147
|
+
const char = withoutComments[index];
|
|
148
|
+
if (inString) {
|
|
149
|
+
normalized += char;
|
|
150
|
+
if (escaped)
|
|
151
|
+
escaped = false;
|
|
152
|
+
else if (char === '\\')
|
|
153
|
+
escaped = true;
|
|
154
|
+
else if (char === '"')
|
|
155
|
+
inString = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char === '"') {
|
|
159
|
+
inString = true;
|
|
160
|
+
normalized += char;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === ',') {
|
|
164
|
+
let lookahead = index + 1;
|
|
165
|
+
while (/\s/.test(withoutComments[lookahead] ?? ''))
|
|
166
|
+
lookahead += 1;
|
|
167
|
+
if (withoutComments[lookahead] === '}' || withoutComments[lookahead] === ']')
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
normalized += char;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(normalized);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
throw new McpConfigShapeError('opencode', source, 'layered configuration is not valid JSON/JSONC');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function entryFromLayeredConfig(raw, source) {
|
|
180
|
+
const parsed = source.endsWith('.jsonc') ? parseReadOnlyJsonc(raw, source) : (() => {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(raw);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new McpConfigShapeError('opencode', source, 'layered configuration is not valid JSON');
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
if (!isRecord(parsed))
|
|
189
|
+
throw new McpConfigShapeError('opencode', source, 'layered configuration must be a JSON object');
|
|
190
|
+
const mcp = parsed['mcp'];
|
|
191
|
+
if (mcp === undefined)
|
|
192
|
+
return { present: false };
|
|
193
|
+
if (!isRecord(mcp))
|
|
194
|
+
throw new McpConfigShapeError('opencode', source, 'mcp must be a JSON object');
|
|
195
|
+
return Object.prototype.hasOwnProperty.call(mcp, 'evolver')
|
|
196
|
+
? { present: true, value: mcp['evolver'] }
|
|
197
|
+
: { present: false };
|
|
198
|
+
}
|
|
199
|
+
function uniquePaths(paths) {
|
|
200
|
+
return [...new Set(paths)];
|
|
201
|
+
}
|
|
202
|
+
function canonicalOpenCodeProjectRoot(configRoot) {
|
|
203
|
+
const directory = resolve(configRoot);
|
|
204
|
+
const parent = dirname(directory);
|
|
205
|
+
if (parent === directory)
|
|
206
|
+
return directory;
|
|
207
|
+
try {
|
|
208
|
+
// Resolve parent aliases such as macOS /tmp, but leave the project-root entry itself for the shared
|
|
209
|
+
// safe-parent guard to reject when configRoot is a caller-controlled symlink.
|
|
210
|
+
return join(realpathSync(parent), basename(directory));
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code !== 'ENOENT')
|
|
214
|
+
throw error;
|
|
215
|
+
return join(canonicalOpenCodeProjectRoot(parent), basename(directory));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function nearestExistingCanonicalDirectory(path) {
|
|
219
|
+
let current = path;
|
|
220
|
+
while (true) {
|
|
221
|
+
try {
|
|
222
|
+
if (lstatSync(current).isDirectory())
|
|
223
|
+
return realpathSync(current);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error.code !== 'ENOENT')
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
const parent = dirname(current);
|
|
230
|
+
if (parent === current)
|
|
231
|
+
return current;
|
|
232
|
+
current = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function isLexicallyContained(root, path) {
|
|
236
|
+
const relativePath = relative(root, path);
|
|
237
|
+
return relativePath === ''
|
|
238
|
+
|| (!isAbsolute(relativePath) && relativePath !== '..' && !relativePath.startsWith(`..${sep}`));
|
|
239
|
+
}
|
|
240
|
+
function filesystemRoot(path) {
|
|
241
|
+
let current = path;
|
|
242
|
+
while (dirname(current) !== current)
|
|
243
|
+
current = dirname(current);
|
|
244
|
+
return current;
|
|
245
|
+
}
|
|
246
|
+
function openCodeWorktreeRoot(configRoot) {
|
|
247
|
+
const directory = canonicalOpenCodeProjectRoot(configRoot);
|
|
248
|
+
try {
|
|
249
|
+
// Keep a caller-controlled root alias as the safe root so the shared guard refuses it before ancestor
|
|
250
|
+
// discovery can retarget a managed config elsewhere in the worktree.
|
|
251
|
+
if (lstatSync(directory).isSymbolicLink())
|
|
252
|
+
return directory;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code !== 'ENOENT')
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
const gitCwd = nearestExistingCanonicalDirectory(directory);
|
|
259
|
+
let output;
|
|
260
|
+
try {
|
|
261
|
+
output = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
262
|
+
cwd: gitCwd,
|
|
263
|
+
encoding: 'utf8',
|
|
264
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
265
|
+
}).trim();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return filesystemRoot(directory);
|
|
269
|
+
}
|
|
270
|
+
if (!output)
|
|
271
|
+
return directory;
|
|
272
|
+
let canonicalWorktreeRoot;
|
|
273
|
+
try {
|
|
274
|
+
canonicalWorktreeRoot = realpathSync(output);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return directory;
|
|
278
|
+
}
|
|
279
|
+
return isLexicallyContained(canonicalWorktreeRoot, directory)
|
|
280
|
+
? canonicalWorktreeRoot
|
|
281
|
+
: directory;
|
|
282
|
+
}
|
|
283
|
+
function openCodeProjectLayerPaths(configRoot, worktreeRoot = openCodeWorktreeRoot(configRoot)) {
|
|
284
|
+
const directory = canonicalOpenCodeProjectRoot(configRoot);
|
|
285
|
+
const ancestors = [];
|
|
286
|
+
let current = directory;
|
|
287
|
+
while (true) {
|
|
288
|
+
ancestors.push(current);
|
|
289
|
+
if (current === worktreeRoot)
|
|
290
|
+
break;
|
|
291
|
+
const parent = dirname(current);
|
|
292
|
+
if (parent === current)
|
|
293
|
+
return [
|
|
294
|
+
join(directory, 'opencode.json'),
|
|
295
|
+
join(directory, 'opencode.jsonc'),
|
|
296
|
+
join(directory, '.opencode', 'opencode.json'),
|
|
297
|
+
join(directory, '.opencode', 'opencode.jsonc'),
|
|
298
|
+
];
|
|
299
|
+
current = parent;
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
...[...ancestors].reverse().flatMap((ancestor) => [
|
|
303
|
+
join(ancestor, 'opencode.json'),
|
|
304
|
+
join(ancestor, 'opencode.jsonc'),
|
|
305
|
+
]),
|
|
306
|
+
...ancestors.flatMap((ancestor) => [
|
|
307
|
+
join(ancestor, '.opencode', 'opencode.json'),
|
|
308
|
+
join(ancestor, '.opencode', 'opencode.jsonc'),
|
|
309
|
+
]),
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
function openCodeLayerPaths(opts, projectDisabled) {
|
|
313
|
+
const home = opts.homeDir ?? homedir();
|
|
314
|
+
const xdgHome = configuredPath(opts.xdgConfigHome ?? process.env['XDG_CONFIG_HOME']) ?? join(home, '.config');
|
|
315
|
+
const globalDir = join(xdgHome, 'opencode');
|
|
316
|
+
const paths = [
|
|
317
|
+
join(globalDir, 'config.json'),
|
|
318
|
+
join(globalDir, 'opencode.json'),
|
|
319
|
+
join(globalDir, 'opencode.jsonc'),
|
|
320
|
+
];
|
|
321
|
+
const explicitConfig = configuredPath(opts.opencodeConfig ?? process.env['OPENCODE_CONFIG']);
|
|
322
|
+
if (explicitConfig)
|
|
323
|
+
paths.push(explicitConfig);
|
|
324
|
+
if ((opts.scope ?? 'project') === 'project' && !projectDisabled) {
|
|
325
|
+
paths.push(...openCodeProjectLayerPaths(opts.configRoot));
|
|
326
|
+
}
|
|
327
|
+
paths.push(join(home, '.opencode', 'opencode.json'), join(home, '.opencode', 'opencode.jsonc'));
|
|
328
|
+
const explicitDir = configuredPath(opts.opencodeConfigDir ?? process.env['OPENCODE_CONFIG_DIR']);
|
|
329
|
+
if (explicitDir)
|
|
330
|
+
paths.push(join(explicitDir, 'opencode.json'), join(explicitDir, 'opencode.jsonc'));
|
|
331
|
+
return uniquePaths(paths);
|
|
332
|
+
}
|
|
333
|
+
function entryFromStrictConfig(raw, source) {
|
|
334
|
+
let parsed;
|
|
335
|
+
try {
|
|
336
|
+
parsed = JSON.parse(raw);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new McpConfigShapeError('opencode', source, 'higher-precedence configuration is not valid strict JSON');
|
|
340
|
+
}
|
|
341
|
+
if (!isRecord(parsed)) {
|
|
342
|
+
throw new McpConfigShapeError('opencode', source, 'higher-precedence configuration must be a JSON object');
|
|
343
|
+
}
|
|
344
|
+
const mcp = parsed['mcp'];
|
|
345
|
+
if (mcp === undefined)
|
|
346
|
+
return { present: false };
|
|
347
|
+
if (!isRecord(mcp)) {
|
|
348
|
+
throw new McpConfigShapeError('opencode', source, 'mcp must be a JSON object');
|
|
349
|
+
}
|
|
350
|
+
return Object.prototype.hasOwnProperty.call(mcp, 'evolver')
|
|
351
|
+
? { present: true, value: mcp['evolver'] }
|
|
352
|
+
: { present: false };
|
|
353
|
+
}
|
|
354
|
+
function openCodeInstallPreflight(resolution, opts, expected) {
|
|
355
|
+
const disableProjectFromOptions = opts.opencodeDisableProjectConfig !== undefined;
|
|
356
|
+
const disableProjectEnvironment = process.env['OPENCODE_DISABLE_PROJECT_CONFIG'];
|
|
357
|
+
const projectDisabled = disableProjectFromOptions
|
|
358
|
+
? opts.opencodeDisableProjectConfig === true
|
|
359
|
+
: ['true', '1'].includes(disableProjectEnvironment?.toLowerCase() ?? '');
|
|
360
|
+
const projectTargetDisabled = projectDisabled
|
|
361
|
+
&& (opts.scope ?? 'project') === 'project'
|
|
362
|
+
&& !configuredPath(opts.opencodeConfigDir ?? process.env['OPENCODE_CONFIG_DIR']);
|
|
363
|
+
const layerPaths = openCodeLayerPaths(opts, projectDisabled);
|
|
364
|
+
const resolvedTargetPath = resolve(resolution.configPath);
|
|
365
|
+
const targetIndex = layerPaths.findIndex((path) => resolve(path) === resolvedTargetPath);
|
|
366
|
+
const layerSnapshots = layerPaths.map(readReadOnlySource);
|
|
367
|
+
let effectiveEntry = { present: false };
|
|
368
|
+
let plannedEffectiveEntry = { present: false };
|
|
369
|
+
for (const [index, snapshot] of layerSnapshots.entries()) {
|
|
370
|
+
const isTarget = resolve(snapshot.path) === resolvedTargetPath;
|
|
371
|
+
const source = isTarget
|
|
372
|
+
? 'target'
|
|
373
|
+
: targetIndex >= 0 && index < targetIndex ? 'lower' : 'higher';
|
|
374
|
+
const entry = snapshot.raw === null
|
|
375
|
+
? { present: false }
|
|
376
|
+
: entryFromLayeredConfig(snapshot.raw, snapshot.path);
|
|
377
|
+
effectiveEntry = mergeEffectiveEntry(effectiveEntry, entry, source);
|
|
378
|
+
plannedEffectiveEntry = mergeEffectiveEntry(plannedEffectiveEntry, isTarget ? { present: true, value: expected } : entry, source);
|
|
379
|
+
}
|
|
380
|
+
const inlineFromOptions = opts.opencodeConfigContent !== undefined;
|
|
381
|
+
const inline = inlineFromOptions ? opts.opencodeConfigContent : process.env['OPENCODE_CONFIG_CONTENT'];
|
|
382
|
+
if (inline) {
|
|
383
|
+
const entry = entryFromStrictConfig(inline, 'OPENCODE_CONFIG_CONTENT');
|
|
384
|
+
effectiveEntry = mergeEffectiveEntry(effectiveEntry, entry, 'higher');
|
|
385
|
+
plannedEffectiveEntry = mergeEffectiveEntry(plannedEffectiveEntry, entry, 'higher');
|
|
386
|
+
}
|
|
387
|
+
const managedDir = opts.opencodeManagedConfigDir
|
|
388
|
+
?? process.env['OPENCODE_TEST_MANAGED_CONFIG_DIR']
|
|
389
|
+
?? resolveOpenCodeManagedConfigDir(opts);
|
|
390
|
+
const managedPaths = [join(managedDir, 'opencode.json'), join(managedDir, 'opencode.jsonc')];
|
|
391
|
+
const managedSnapshots = managedPaths.map(readReadOnlySource);
|
|
392
|
+
for (const snapshot of managedSnapshots) {
|
|
393
|
+
if (snapshot.raw === null)
|
|
394
|
+
continue;
|
|
395
|
+
const entry = entryFromLayeredConfig(snapshot.raw, snapshot.path);
|
|
396
|
+
effectiveEntry = mergeEffectiveEntry(effectiveEntry, entry, 'higher');
|
|
397
|
+
plannedEffectiveEntry = mergeEffectiveEntry(plannedEffectiveEntry, entry, 'higher');
|
|
398
|
+
}
|
|
399
|
+
const preferencePaths = opts.opencodeManagedPreferencePaths ?? resolveOpenCodeManagedPreferencePaths(opts);
|
|
400
|
+
if (preferencePaths.some(existsSync)) {
|
|
401
|
+
throw new McpConfigOwnershipError('opencode', 'macOS managed preferences are active and cannot be safely inspected without invoking system tooling');
|
|
402
|
+
}
|
|
403
|
+
const effectiveMatches = effectiveEntry.present && valuesEqual(effectiveEntry.value, expected);
|
|
404
|
+
const plannedEffectiveMatches = plannedEffectiveEntry.present
|
|
405
|
+
&& valuesEqual(plannedEffectiveEntry.value, expected);
|
|
406
|
+
if (!projectTargetDisabled && !effectiveMatches && !plannedEffectiveMatches) {
|
|
407
|
+
throw new McpConfigConflictError('opencode', 'mcp.evolver', expected, plannedEffectiveEntry.present ? plannedEffectiveEntry.value : '<missing>');
|
|
408
|
+
}
|
|
409
|
+
if (effectiveEntry.present && !effectiveMatches && effectiveEntry.source !== 'target') {
|
|
410
|
+
const forceCanOverride = opts.force === true && !projectTargetDisabled;
|
|
411
|
+
if (!forceCanOverride) {
|
|
412
|
+
throw new McpConfigConflictError('opencode', 'mcp.evolver', expected, effectiveEntry.value);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (projectTargetDisabled && !effectiveMatches) {
|
|
416
|
+
throw new McpConfigConflictError('opencode', 'mcp.evolver', expected, effectiveEntry.present ? effectiveEntry.value : '<missing while OPENCODE_DISABLE_PROJECT_CONFIG is enabled>');
|
|
417
|
+
}
|
|
418
|
+
const alreadyInstalled = effectiveMatches && effectiveEntry.source !== 'target';
|
|
419
|
+
const guardedLayerSnapshots = alreadyInstalled
|
|
420
|
+
? layerSnapshots
|
|
421
|
+
: layerSnapshots.filter((snapshot) => resolve(snapshot.path) !== resolvedTargetPath);
|
|
422
|
+
return {
|
|
423
|
+
alreadyInstalled,
|
|
424
|
+
assertUnchanged() {
|
|
425
|
+
if (!inlineFromOptions && process.env['OPENCODE_CONFIG_CONTENT'] !== inline) {
|
|
426
|
+
throw new McpConfigChangedError('opencode', 'OPENCODE_CONFIG_CONTENT');
|
|
427
|
+
}
|
|
428
|
+
if (!disableProjectFromOptions
|
|
429
|
+
&& process.env['OPENCODE_DISABLE_PROJECT_CONFIG'] !== disableProjectEnvironment) {
|
|
430
|
+
throw new McpConfigChangedError('opencode', 'OPENCODE_DISABLE_PROJECT_CONFIG');
|
|
431
|
+
}
|
|
432
|
+
for (const snapshot of [...guardedLayerSnapshots, ...managedSnapshots]) {
|
|
433
|
+
const current = readReadOnlySource(snapshot.path);
|
|
434
|
+
if (current.raw !== snapshot.raw)
|
|
435
|
+
throw new McpConfigChangedError('opencode', snapshot.path);
|
|
436
|
+
}
|
|
437
|
+
if (preferencePaths.some(existsSync))
|
|
438
|
+
throw new McpConfigChangedError('opencode', 'macOS managed preferences');
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/** Resolve the OpenCode file that is active at the requested scope.
|
|
443
|
+
* OpenCode layers project files from the worktree root down to the current directory, or from the filesystem root
|
|
444
|
+
* outside Git, then .opencode directories in reverse order. JSONC follows JSON at every location. Global,
|
|
445
|
+
* explicit, home, and managed layers retain their surrounding precedence. A JSONC path is writable only when its
|
|
446
|
+
* contents are strict JSON;
|
|
447
|
+
* comment/trailing-comma syntax is rejected by the shared strict parser so setup never rewrites it lossy.
|
|
448
|
+
*/
|
|
449
|
+
export function resolveOpenCodeConfig(opts) {
|
|
450
|
+
const scope = opts.scope ?? 'project';
|
|
451
|
+
const explicitDir = configuredPath(opts.opencodeConfigDir ?? process.env['OPENCODE_CONFIG_DIR']);
|
|
452
|
+
if (explicitDir) {
|
|
453
|
+
const jsonPath = join(explicitDir, 'opencode.json');
|
|
454
|
+
const jsoncPath = join(explicitDir, 'opencode.jsonc');
|
|
455
|
+
return {
|
|
456
|
+
configPath: existsSync(jsoncPath) ? jsoncPath : jsonPath,
|
|
457
|
+
safeRoot: explicitDir,
|
|
458
|
+
conflictingPaths: [],
|
|
459
|
+
evidencePaths: [explicitDir, jsonPath, jsoncPath],
|
|
460
|
+
topologyCandidatePaths: [jsonPath, jsoncPath],
|
|
461
|
+
uninstallCandidatePaths: [jsonPath, jsoncPath],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (scope === 'user') {
|
|
465
|
+
const home = opts.homeDir ?? homedir();
|
|
466
|
+
const explicitConfig = configuredPath(opts.opencodeConfig ?? process.env['OPENCODE_CONFIG']);
|
|
467
|
+
if (explicitConfig) {
|
|
468
|
+
return {
|
|
469
|
+
configPath: explicitConfig,
|
|
470
|
+
safeRoot: dirname(explicitConfig),
|
|
471
|
+
conflictingPaths: [],
|
|
472
|
+
evidencePaths: [explicitConfig],
|
|
473
|
+
topologyCandidatePaths: [explicitConfig],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const xdgHome = configuredPath(opts.xdgConfigHome ?? process.env['XDG_CONFIG_HOME']) ?? join(home, '.config');
|
|
477
|
+
const directory = join(xdgHome, 'opencode');
|
|
478
|
+
const jsonPath = join(directory, 'opencode.json');
|
|
479
|
+
const jsoncPath = join(directory, 'opencode.jsonc');
|
|
480
|
+
return {
|
|
481
|
+
configPath: existsSync(jsoncPath) ? jsoncPath : jsonPath,
|
|
482
|
+
safeRoot: directory,
|
|
483
|
+
conflictingPaths: [],
|
|
484
|
+
evidencePaths: [directory, jsonPath, jsoncPath],
|
|
485
|
+
topologyCandidatePaths: [jsonPath, jsoncPath],
|
|
486
|
+
uninstallCandidatePaths: [jsonPath, jsoncPath],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const projectRoot = canonicalOpenCodeProjectRoot(opts.configRoot);
|
|
490
|
+
const rootJson = join(projectRoot, 'opencode.json');
|
|
491
|
+
const rootJsonc = join(projectRoot, 'opencode.jsonc');
|
|
492
|
+
const nestedJson = join(projectRoot, '.opencode', 'opencode.json');
|
|
493
|
+
const nestedJsonc = join(projectRoot, '.opencode', 'opencode.jsonc');
|
|
494
|
+
const target = existsSync(nestedJsonc)
|
|
495
|
+
? nestedJsonc
|
|
496
|
+
: existsSync(nestedJson)
|
|
497
|
+
? nestedJson
|
|
498
|
+
: existsSync(rootJsonc)
|
|
499
|
+
? rootJsonc
|
|
500
|
+
: rootJson;
|
|
501
|
+
const userResolution = resolveOpenCodeConfig({ ...opts, scope: 'user' });
|
|
502
|
+
const worktreeRoot = openCodeWorktreeRoot(projectRoot);
|
|
503
|
+
const projectLayerPaths = openCodeProjectLayerPaths(projectRoot, worktreeRoot);
|
|
504
|
+
return {
|
|
505
|
+
configPath: target,
|
|
506
|
+
safeRoot: projectRoot,
|
|
507
|
+
conflictingPaths: [],
|
|
508
|
+
evidencePaths: [...(userResolution.evidencePaths ?? []), ...projectLayerPaths],
|
|
509
|
+
topologyCandidatePaths: [rootJson, rootJsonc, nestedJson, nestedJsonc],
|
|
510
|
+
uninstallCandidatePaths: [rootJson, rootJsonc, nestedJson, nestedJsonc],
|
|
511
|
+
installDiscoveryPaths: projectLayerPaths,
|
|
512
|
+
installSafeRoot: worktreeRoot,
|
|
513
|
+
uninstallDiscoveryPaths: projectLayerPaths,
|
|
514
|
+
uninstallSafeRoot: worktreeRoot,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
export const OPENCODE_SPEC = {
|
|
518
|
+
runtime: 'opencode',
|
|
519
|
+
configPath: (opts) => resolveOpenCodeConfig(opts).configPath,
|
|
520
|
+
resolveConfig: resolveOpenCodeConfig,
|
|
521
|
+
installPreflight: openCodeInstallPreflight,
|
|
522
|
+
containerKey: 'mcp',
|
|
523
|
+
entry: (server) => ({
|
|
524
|
+
type: 'local',
|
|
525
|
+
command: [server.command, ...(server.args ?? [])],
|
|
526
|
+
...(server.env && Object.keys(server.env).length > 0 ? { environment: server.env } : {}),
|
|
527
|
+
enabled: true,
|
|
528
|
+
}),
|
|
529
|
+
};
|
|
530
|
+
export const installOpenCode = installJsonMcpRuntime.bind(undefined, OPENCODE_SPEC);
|
|
531
|
+
export const uninstallOpenCode = uninstallJsonMcpRuntime.bind(undefined, OPENCODE_SPEC);
|
package/dist/primer.js
CHANGED
|
@@ -14,13 +14,16 @@ export function buildEvolverPrimer(opts = {}) {
|
|
|
14
14
|
? 'dry-run validate it (evolver_asset_validate), then publish (evolver_asset_publish).'
|
|
15
15
|
: 'then publish it (evolver_asset_publish).';
|
|
16
16
|
const lines = [
|
|
17
|
-
'Evolver gives this agent reusable memory
|
|
17
|
+
'Evolver gives this agent reusable memory. A Recipe is ordered Gene/Capsule DNA. Default discovery/execution is Recipe search then express; Gene/Capsule search is fallback when no Recipe matches. Expressing a Recipe is what actually reuses those steps on the hub. Use it quietly when prior experience is likely to help:',
|
|
18
18
|
'',
|
|
19
|
-
'1.
|
|
20
|
-
' - call evolver_recall when approved local genes are likely to help;',
|
|
21
|
-
` - call evolver_asset_search with concise key signals or error text to search ${searchWhere};`,
|
|
22
|
-
' - if a candidate fits, call evolver_asset_fetch and reuse only the parts that apply.',
|
|
19
|
+
'1. SEARCH AND EXPRESS RECIPES FIRST. For clear error text, repeated workflows, or substantial tasks:',
|
|
23
20
|
];
|
|
21
|
+
if (proxy) {
|
|
22
|
+
lines.push(' - call evolver_recipe_search with the task or error text;', ' - if a recipe fits, call evolver_recipe_express — the hub expands Gene then Capsule steps; do not parse recipe JSON locally;', ' - if no recipe hits, fall back to evolver_asset_search / evolver_asset_fetch on genes and capsules;', ' - call evolver_recall only for approved local genes that are likely to help.');
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(' - call evolver_recall when approved local genes are likely to help;', ` - call evolver_asset_search with concise key signals or error text to search ${searchWhere} (Recipe search needs a hub/proxy);`, ' - if a candidate fits, call evolver_asset_fetch and reuse only the parts that apply.');
|
|
26
|
+
}
|
|
24
27
|
if (proxy) {
|
|
25
28
|
lines.push('2. REPORT REAL REUSE. After a fetched asset materially affects the solution, call evolver_asset_reuse_result', ' (success / failed / mismatched / stale / unsafe) so the memory learns what is worth keeping.', '3. CAPTURE VERIFIED LEARNING. When you solve something non-trivial and have VERIFIED it, distill it for the next agent:');
|
|
26
29
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const PRODUCT_BRIDGE_SERVER_ID = "evox-product";
|
|
2
|
+
export declare const PRODUCT_BRIDGE_MANAGED_KEY = "_evox_product_managed";
|
|
3
|
+
export declare const PRODUCT_BRIDGE_PREVIOUS_KEY = "_evox_product_previous";
|
|
4
|
+
export declare const PRODUCT_BRIDGE_GRANT_SCHEMA = "evox.product_bridge.grant.v1";
|
|
5
|
+
/** Resolve only a compiled JavaScript shim. Source TypeScript is never written into runtime config. */
|
|
6
|
+
export declare function productBridgeShimPath(): string;
|
|
7
|
+
/** Ownership is explicit. A matching filename alone is never enough to delete a user's server. */
|
|
8
|
+
export declare function isOwnedProductBridge(entry: unknown): boolean;
|
|
9
|
+
/** Restore a user entry previously preserved by an explicit force takeover. */
|
|
10
|
+
export declare function restoreProductBridgeEntry(entry: unknown): {
|
|
11
|
+
restored: boolean;
|
|
12
|
+
entry?: unknown;
|
|
13
|
+
};
|
|
14
|
+
/** Merge a managed evox-product server into a parsed MCP JSON object (project .mcp.json or ~/.claude.json). */
|
|
15
|
+
export declare function withClaudeProductBridge(data: Record<string, unknown>, force?: boolean): {
|
|
16
|
+
changed: boolean;
|
|
17
|
+
skipped?: boolean;
|
|
18
|
+
data: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
/** Merge a managed evox-product table into parsed Codex TOML. */
|
|
21
|
+
export declare function withCodexProductBridge(data: Record<string, unknown>, force?: boolean): {
|
|
22
|
+
changed: boolean;
|
|
23
|
+
skipped?: boolean;
|
|
24
|
+
data: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
export declare function installClaudeProductBridge(configRoot: string, force?: boolean): {
|
|
27
|
+
changed: boolean;
|
|
28
|
+
skipped?: boolean;
|
|
29
|
+
path: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function uninstallClaudeProductBridge(configRoot: string): boolean;
|
|
32
|
+
export declare function installCodexProductBridge(configRoot: string, force?: boolean): {
|
|
33
|
+
changed: boolean;
|
|
34
|
+
skipped?: boolean;
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function uninstallCodexProductBridge(configRoot: string): boolean;
|