chati-dev 4.5.7 → 4.5.9
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/framework/agents/build/dev.md +1 -1
- package/framework/agents/deploy/devops.md +1 -1
- package/framework/agents/discover/brief.md +1 -1
- package/framework/agents/discover/brownfield-wu.md +1 -1
- package/framework/agents/discover/greenfield-wu.md +1 -1
- package/framework/agents/plan/architect-data-engineer.md +1 -1
- package/framework/agents/plan/architect-system.md +1 -1
- package/framework/agents/plan/architect.md +1 -1
- package/framework/agents/plan/detail.md +1 -1
- package/framework/agents/plan/phases.md +1 -1
- package/framework/agents/plan/tasks.md +1 -1
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux-component-engineer.md +1 -1
- package/framework/agents/plan/ux-researcher.md +1 -1
- package/framework/agents/plan/ux.md +1 -1
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/agents/shared/visualizer.md +1 -1
- package/framework/config.yaml +2 -2
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/hooks/license-guard.js +19 -0
- package/framework/manifest.json +53 -48
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +177 -71
- package/framework/orchestrator/chati.md +1 -1
- package/framework/package.json +3 -0
- package/package.json +1 -1
- package/src/config/claude-settings-generator.js +6 -6
- package/src/config/context-file-generator.js +2 -2
- package/src/config/ide-configs.js +3 -3
- package/src/config/mcp-configs.js +80 -10
- package/src/installer/core.js +60 -34
- package/src/installer/path-replacement.js +13 -0
- package/src/installer/provider-overlay.js +3 -1
- package/src/installer-v2/index.js +55 -0
|
@@ -21,6 +21,125 @@ import { join } from 'path';
|
|
|
21
21
|
const PROJECT_DIR = process.cwd();
|
|
22
22
|
const ROUTER_ARGS = process.argv.slice(2);
|
|
23
23
|
|
|
24
|
+
async function readEnabledHarnesses(projectDir) {
|
|
25
|
+
const installationPath = join(projectDir, '.chati', 'v2', 'installation.json');
|
|
26
|
+
if (existsSync(installationPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const artifact = JSON.parse(readFileSync(installationPath, 'utf-8'));
|
|
29
|
+
const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
30
|
+
const doctorModulePath = join(projectDir, fwDir, '_cli', 'installer-v2', 'index.js');
|
|
31
|
+
if (!existsSync(doctorModulePath)) return { harnesses: [], error: 'v2 doctor missing' };
|
|
32
|
+
const { doctorV2 } = await import(doctorModulePath);
|
|
33
|
+
if (!doctorV2(artifact).passed) return { harnesses: [], error: 'v2 artifact invalid' };
|
|
34
|
+
const harnesses = artifact.installation?.enabled_providers
|
|
35
|
+
?.map((binding) => binding.harness_id)
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
if (harnesses?.length) return { harnesses: [...new Set(harnesses)], error: null };
|
|
38
|
+
} catch { return { harnesses: [], error: 'v2 artifact invalid' }; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const detected = [];
|
|
42
|
+
if (existsSync(join(projectDir, '.claude', 'commands', 'chati.md'))) detected.push('claude');
|
|
43
|
+
if (existsSync(join(projectDir, '.agents', 'skills', 'chati', 'SKILL.md'))) detected.push('codex');
|
|
44
|
+
if (existsSync(join(projectDir, '.grok', 'commands', 'chati.md'))) detected.push('grok');
|
|
45
|
+
return { harnesses: detected, error: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasContent(path) {
|
|
49
|
+
return existsSync(path) && readFileSync(path, 'utf-8').trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function codexHooksEnabled(config) {
|
|
53
|
+
if (/^\s*features\.hooks\s*=\s*true\s*(?:#.*)?$/m.test(config)) return true;
|
|
54
|
+
const lines = config.split('\n');
|
|
55
|
+
const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
|
|
56
|
+
if (sectionStart === -1) return false;
|
|
57
|
+
const sectionEnd = lines.findIndex((line, index) => index > sectionStart && /^\s*\[.*\]\s*(?:#.*)?$/.test(line));
|
|
58
|
+
const end = sectionEnd === -1 ? lines.length : sectionEnd;
|
|
59
|
+
return lines.slice(sectionStart + 1, end).some((line) => /^\s*hooks\s*=\s*true\s*(?:#.*)?$/.test(line));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function validateProviderIntegrity(projectDir) {
|
|
63
|
+
const missing = [];
|
|
64
|
+
const { harnesses, error } = await readEnabledHarnesses(projectDir);
|
|
65
|
+
|
|
66
|
+
if (error) missing.push(error);
|
|
67
|
+
const supportedHarnesses = new Set(['claude', 'codex', 'grok']);
|
|
68
|
+
for (const harness of harnesses) {
|
|
69
|
+
if (!supportedHarnesses.has(harness)) missing.push(`unsupported harness ${harness}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (harnesses.length === 0) missing.push('provider entry point');
|
|
73
|
+
|
|
74
|
+
if (harnesses.includes('claude')) {
|
|
75
|
+
const commandPath = join(projectDir, '.claude', 'commands', 'chati.md');
|
|
76
|
+
const settingsPath = join(projectDir, '.claude', 'settings.json');
|
|
77
|
+
if (!hasContent(commandPath)) missing.push('claude command');
|
|
78
|
+
if (!existsSync(settingsPath)) {
|
|
79
|
+
missing.push('claude settings');
|
|
80
|
+
} else {
|
|
81
|
+
try {
|
|
82
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
83
|
+
const ptu = settings.hooks?.PreToolUse || [];
|
|
84
|
+
const ups = settings.hooks?.UserPromptSubmit || [];
|
|
85
|
+
const ptuCmds = ptu.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
|
|
86
|
+
const upsCmds = ups.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
|
|
87
|
+
missing.push(...['mode-governance', 'constitution-guard', 'read-protection']
|
|
88
|
+
.filter((hook) => !ptuCmds.some((command) => command.includes(hook)))
|
|
89
|
+
.map((hook) => `claude hook ${hook}`));
|
|
90
|
+
if (![...upsCmds, ...ptuCmds].some((command) => command.includes('license-guard'))) {
|
|
91
|
+
missing.push('claude hook license-guard');
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
missing.push('claude settings invalid');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (harnesses.includes('codex')) {
|
|
100
|
+
const required = [
|
|
101
|
+
['codex skill', '.agents/skills/chati/SKILL.md'],
|
|
102
|
+
['codex constitution guard', '.codex/rules/constitution-guard.rules'],
|
|
103
|
+
['codex read protection', '.codex/rules/read-protection.rules'],
|
|
104
|
+
];
|
|
105
|
+
for (const [label, relativePath] of required) {
|
|
106
|
+
if (!hasContent(join(projectDir, relativePath))) missing.push(label);
|
|
107
|
+
}
|
|
108
|
+
const configPath = join(projectDir, '.codex', 'config.toml');
|
|
109
|
+
if (!hasContent(configPath) || !codexHooksEnabled(readFileSync(configPath, 'utf-8'))) {
|
|
110
|
+
missing.push('codex hooks feature');
|
|
111
|
+
}
|
|
112
|
+
const hooksPath = join(projectDir, '.codex', 'hooks.json');
|
|
113
|
+
if (!hasContent(hooksPath)) {
|
|
114
|
+
missing.push('codex license hook');
|
|
115
|
+
} else {
|
|
116
|
+
try {
|
|
117
|
+
const hooks = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
|
118
|
+
const validHook = (hooks.hooks?.UserPromptSubmit || []).some((group) =>
|
|
119
|
+
group.matcher === '.*'
|
|
120
|
+
&& (group.hooks || []).some((hook) =>
|
|
121
|
+
hook.type === 'command'
|
|
122
|
+
&& hook.command === 'node .chati.dev/hooks/license-guard.js'
|
|
123
|
+
&& hook.async !== true
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
if (!validHook) {
|
|
127
|
+
missing.push('codex license hook');
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
missing.push('codex hooks invalid');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (harnesses.includes('grok')) {
|
|
136
|
+
if (!hasContent(join(projectDir, '.grok', 'commands', 'chati.md'))) missing.push('grok command');
|
|
137
|
+
if (!hasContent(join(projectDir, '.grok', 'session-lock.md'))) missing.push('grok session lock');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { valid: missing.length === 0, missing };
|
|
141
|
+
}
|
|
142
|
+
|
|
24
143
|
/**
|
|
25
144
|
* Find the chati-dev CLI module. Tries local node_modules first (fast),
|
|
26
145
|
* then monorepo dev layout. Returns the module or null.
|
|
@@ -245,9 +364,64 @@ async function dispatchOrchestrate(subCommand, argv) {
|
|
|
245
364
|
}
|
|
246
365
|
}
|
|
247
366
|
|
|
367
|
+
async function runSecurityPreflight(projectDir) {
|
|
368
|
+
const result = { ok: true, license: null, integrity: true, error: null };
|
|
369
|
+
try {
|
|
370
|
+
const integrity = await validateProviderIntegrity(projectDir);
|
|
371
|
+
if (!integrity.valid) {
|
|
372
|
+
return {
|
|
373
|
+
...result,
|
|
374
|
+
ok: false,
|
|
375
|
+
integrity: false,
|
|
376
|
+
error: `integrity_failed: missing ${integrity.missing.join(', ')}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
return { ...result, ok: false, integrity: false, error: 'integrity_failed' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
385
|
+
const hookPath = join(projectDir, fwDir, 'hooks', 'license-guard.js');
|
|
386
|
+
if (!existsSync(hookPath)) {
|
|
387
|
+
return {
|
|
388
|
+
...result,
|
|
389
|
+
ok: false,
|
|
390
|
+
error: 'license_guard_missing',
|
|
391
|
+
license: { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const mod = await import(hookPath);
|
|
395
|
+
result.license = await mod.checkLicense();
|
|
396
|
+
if (!result.license.valid) return { ...result, ok: false, error: 'license_invalid' };
|
|
397
|
+
} catch {
|
|
398
|
+
return {
|
|
399
|
+
...result,
|
|
400
|
+
ok: false,
|
|
401
|
+
error: 'license_check_error',
|
|
402
|
+
license: { valid: false, error: 'license_check_error' },
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Only read-only diagnosis and license recovery may run without a valid
|
|
410
|
+
// installation preflight. `init` mutates session state and must never bypass
|
|
411
|
+
// license or provider-integrity enforcement.
|
|
412
|
+
const PREFLIGHT_EXEMPT_SUBS = new Set(['doctor', 'wait-for-license']);
|
|
413
|
+
|
|
248
414
|
async function main() {
|
|
249
415
|
const subCommand = ROUTER_ARGS[0];
|
|
250
416
|
|
|
417
|
+
const preflight = subCommand && PREFLIGHT_EXEMPT_SUBS.has(subCommand)
|
|
418
|
+
? { ok: true, license: null, integrity: true, error: null }
|
|
419
|
+
: await runSecurityPreflight(PROJECT_DIR);
|
|
420
|
+
if (!preflight.ok) {
|
|
421
|
+
console.log(JSON.stringify(preflight));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
251
425
|
// Sub-command mode: `node chati-router.js <subcommand> [--flags...]`
|
|
252
426
|
// Routes to the local CLI for ANY orchestrate subcommand. No npx, no network.
|
|
253
427
|
if (subCommand && VALID_ORCHESTRATE_SUBS.has(subCommand)) {
|
|
@@ -271,8 +445,8 @@ async function main() {
|
|
|
271
445
|
// No-subcommand mode (default): full router pipeline (license + integrity + session + next)
|
|
272
446
|
const result = {
|
|
273
447
|
ok: true,
|
|
274
|
-
license:
|
|
275
|
-
integrity:
|
|
448
|
+
license: preflight.license,
|
|
449
|
+
integrity: preflight.integrity,
|
|
276
450
|
session: null,
|
|
277
451
|
action: null,
|
|
278
452
|
pipeline: null,
|
|
@@ -281,75 +455,7 @@ async function main() {
|
|
|
281
455
|
};
|
|
282
456
|
|
|
283
457
|
// -----------------------------------------------------------------------
|
|
284
|
-
// 1.
|
|
285
|
-
// -----------------------------------------------------------------------
|
|
286
|
-
try {
|
|
287
|
-
const fwDir = existsSync(join(PROJECT_DIR, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
288
|
-
const hookPath = join(PROJECT_DIR, fwDir, 'hooks', 'license-guard.js');
|
|
289
|
-
if (!existsSync(hookPath)) {
|
|
290
|
-
result.ok = false;
|
|
291
|
-
result.error = 'license_guard_missing';
|
|
292
|
-
result.license = { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' };
|
|
293
|
-
console.log(JSON.stringify(result));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const mod = await import(hookPath);
|
|
298
|
-
const license = await mod.checkLicense();
|
|
299
|
-
result.license = license;
|
|
300
|
-
|
|
301
|
-
if (!license.valid) {
|
|
302
|
-
result.ok = false;
|
|
303
|
-
result.error = 'license_invalid';
|
|
304
|
-
console.log(JSON.stringify(result));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
} catch (err) {
|
|
308
|
-
// Fail-closed on license check errors
|
|
309
|
-
result.license = { valid: false, error: 'license_check_error' };
|
|
310
|
-
result.ok = false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// -----------------------------------------------------------------------
|
|
314
|
-
// 2. Integrity check — are all required hooks registered in settings.json?
|
|
315
|
-
// Note: license-guard is registered under UserPromptSubmit (not PreToolUse).
|
|
316
|
-
// mode-governance, constitution-guard, and read-protection are in PreToolUse.
|
|
317
|
-
// -----------------------------------------------------------------------
|
|
318
|
-
try {
|
|
319
|
-
const settingsPath = join(PROJECT_DIR, '.claude', 'settings.json');
|
|
320
|
-
if (existsSync(settingsPath)) {
|
|
321
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
322
|
-
const ptu = settings.hooks?.PreToolUse || [];
|
|
323
|
-
const ups = settings.hooks?.UserPromptSubmit || [];
|
|
324
|
-
const ptuCmds = ptu.flatMap(g => (g.hooks || []).map(h => h.command || ''));
|
|
325
|
-
const upsCmds = ups.flatMap(g => (g.hooks || []).map(h => h.command || ''));
|
|
326
|
-
// license-guard lives in UserPromptSubmit; others live in PreToolUse
|
|
327
|
-
const missingHooks = [
|
|
328
|
-
...(['mode-governance', 'constitution-guard', 'read-protection'].filter(h => !ptuCmds.some(c => c.includes(h)))),
|
|
329
|
-
...(['license-guard'].filter(h => !upsCmds.some(c => c.includes(h)) && !ptuCmds.some(c => c.includes(h)))),
|
|
330
|
-
];
|
|
331
|
-
result.integrity = missingHooks.length === 0;
|
|
332
|
-
if (missingHooks.length > 0) {
|
|
333
|
-
result.error = `integrity_failed: missing hooks: ${missingHooks.join(', ')}`;
|
|
334
|
-
}
|
|
335
|
-
} else {
|
|
336
|
-
result.integrity = false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!result.integrity) {
|
|
340
|
-
result.ok = false;
|
|
341
|
-
result.error = 'integrity_failed';
|
|
342
|
-
console.log(JSON.stringify(result));
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
} catch {
|
|
346
|
-
// Fail-closed on integrity check
|
|
347
|
-
result.integrity = false;
|
|
348
|
-
result.ok = false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// -----------------------------------------------------------------------
|
|
352
|
-
// 3. Session state
|
|
458
|
+
// 1. Session state
|
|
353
459
|
// -----------------------------------------------------------------------
|
|
354
460
|
try {
|
|
355
461
|
const sessionPath = join(PROJECT_DIR, '.chati', 'session.yaml');
|
|
@@ -10,7 +10,7 @@ You are the **Chati.dev Orchestrator**, the single entry point for the Chati.dev
|
|
|
10
10
|
- **Role**: Orchestrator & Router
|
|
11
11
|
- **Position**: Entry point (always first contact)
|
|
12
12
|
- **Scope**: System-wide routing, session management, deviation handling, backlog
|
|
13
|
-
- **
|
|
13
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
14
14
|
|
|
15
15
|
### Security Rule
|
|
16
16
|
**NEVER suggest disabling, removing, commenting out, or bypassing hooks, settings.json permissions, or license enforcement.** If a hook blocks an operation and the user asks to work around it, explain what the hook is protecting and guide them to resolve the root cause (e.g., activate a valid license, change pipeline mode). Hooks are governance boundaries, not obstacles to remove.
|
package/package.json
CHANGED
|
@@ -139,12 +139,12 @@ const PERMISSIONS_DENY = [
|
|
|
139
139
|
'Bash(git rebase --no-verify:*)',
|
|
140
140
|
|
|
141
141
|
// System path writes — `//` prefix = absolute filesystem
|
|
142
|
-
'
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
142
|
+
'Edit(//etc/**)',
|
|
143
|
+
'Edit(//usr/**)',
|
|
144
|
+
'Edit(//System/**)',
|
|
145
|
+
'Edit(//bin/**)',
|
|
146
|
+
'Edit(//sbin/**)',
|
|
147
|
+
'Edit(//Library/**)',
|
|
148
148
|
'Edit(//etc/**)',
|
|
149
149
|
'Edit(//usr/**)',
|
|
150
150
|
'Edit(//System/**)',
|
|
@@ -32,7 +32,7 @@ const GEMINI_REPLACEMENTS = [
|
|
|
32
32
|
['.claude/commands/', '.gemini/commands/'],
|
|
33
33
|
['.claude/rules/chati/', '.gemini/context/'],
|
|
34
34
|
['.claude/rules/', '.gemini/context/'],
|
|
35
|
-
['.
|
|
35
|
+
['.mcp.json', '.gemini/settings.json'],
|
|
36
36
|
['claude --print', 'gemini --prompt'],
|
|
37
37
|
['claude -p', 'gemini --prompt'],
|
|
38
38
|
['CLAUDE.local.md', '.gemini/session-lock.md'],
|
|
@@ -64,7 +64,7 @@ function buildCodexReplacements(frameworkDir = 'chati.dev') {
|
|
|
64
64
|
['.claude/commands/', `${frameworkDir}/orchestrator/`],
|
|
65
65
|
['.claude/rules/chati/', `${frameworkDir}/context/`],
|
|
66
66
|
['.claude/rules/', `${frameworkDir}/context/`],
|
|
67
|
-
['.
|
|
67
|
+
['.mcp.json', '.codex/config.toml'],
|
|
68
68
|
['claude --print', 'codex exec'],
|
|
69
69
|
['claude -p', 'codex exec'],
|
|
70
70
|
['CLAUDE.local.md', 'AGENTS.override.md'],
|
|
@@ -10,7 +10,7 @@ export const IDE_CONFIGS = {
|
|
|
10
10
|
recommended: true,
|
|
11
11
|
configPath: '.claude/commands/',
|
|
12
12
|
rulesFile: '.claude/CLAUDE.md',
|
|
13
|
-
mcpConfigFile: '.
|
|
13
|
+
mcpConfigFile: '.mcp.json',
|
|
14
14
|
formatNotes: 'Native markdown format',
|
|
15
15
|
},
|
|
16
16
|
'gemini-cli': {
|
|
@@ -32,7 +32,7 @@ export const IDE_CONFIGS = {
|
|
|
32
32
|
recommended: false,
|
|
33
33
|
configPath: '.agents/skills/chati/',
|
|
34
34
|
rulesFile: 'AGENTS.md',
|
|
35
|
-
mcpConfigFile:
|
|
35
|
+
mcpConfigFile: '.codex/config.toml',
|
|
36
36
|
formatNotes: 'Codex skill format (SKILL.md with YAML frontmatter)',
|
|
37
37
|
rulesPath: '.codex/rules/',
|
|
38
38
|
overrideFile: 'AGENTS.override.md',
|
|
@@ -44,7 +44,7 @@ export const IDE_CONFIGS = {
|
|
|
44
44
|
recommended: false,
|
|
45
45
|
configPath: '.grok/commands/',
|
|
46
46
|
rulesFile: 'AGENTS.md',
|
|
47
|
-
mcpConfigFile: '.grok/
|
|
47
|
+
mcpConfigFile: '.grok/config.toml',
|
|
48
48
|
formatNotes: 'Native markdown command format',
|
|
49
49
|
},
|
|
50
50
|
'vscode': {
|
|
@@ -10,7 +10,7 @@ export const MCP_CONFIGS = {
|
|
|
10
10
|
name: 'Browser (Playwright)',
|
|
11
11
|
description: 'Web automation and testing',
|
|
12
12
|
requiresEnv: [],
|
|
13
|
-
|
|
13
|
+
serverConfig: {
|
|
14
14
|
command: 'npx',
|
|
15
15
|
args: ['-y', '@playwright/mcp'],
|
|
16
16
|
},
|
|
@@ -19,13 +19,16 @@ export const MCP_CONFIGS = {
|
|
|
19
19
|
name: 'Context7',
|
|
20
20
|
description: 'Library documentation search',
|
|
21
21
|
requiresEnv: [],
|
|
22
|
-
|
|
22
|
+
serverConfig: {
|
|
23
23
|
command: 'npx',
|
|
24
|
-
args: ['-y', '@context7
|
|
24
|
+
args: ['-y', '@upstash/context7-mcp'],
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
export const MCP_TOML_BEGIN = '# BEGIN CHATI MANAGED MCP SERVERS';
|
|
30
|
+
export const MCP_TOML_END = '# END CHATI MANAGED MCP SERVERS';
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* Agent-MCP dependency matrix
|
|
31
34
|
*/
|
|
@@ -47,22 +50,89 @@ export const AGENT_MCP_DEPS = {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
|
-
*
|
|
53
|
+
* Resolve the provider-neutral server definitions selected by the user.
|
|
51
54
|
*/
|
|
52
|
-
export function
|
|
55
|
+
export function resolveSelectedMCPServers(selectedMCPs) {
|
|
53
56
|
const mcpServers = {};
|
|
54
57
|
for (const mcpKey of selectedMCPs) {
|
|
55
58
|
const config = MCP_CONFIGS[mcpKey];
|
|
56
|
-
if (config?.
|
|
59
|
+
if (config?.serverConfig) {
|
|
57
60
|
const entry = {
|
|
58
|
-
command: config.
|
|
59
|
-
args: config.
|
|
61
|
+
command: config.serverConfig.command,
|
|
62
|
+
args: [...(config.serverConfig.args || [])],
|
|
60
63
|
};
|
|
61
|
-
if (config.
|
|
62
|
-
entry.env = config.
|
|
64
|
+
if (config.serverConfig.env) {
|
|
65
|
+
entry.env = { ...config.serverConfig.env };
|
|
63
66
|
}
|
|
64
67
|
mcpServers[mcpKey] = entry;
|
|
65
68
|
}
|
|
66
69
|
}
|
|
70
|
+
return mcpServers;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Generate Claude Code's project-scoped root .mcp.json. */
|
|
74
|
+
export function generateClaudeMCPConfig(selectedMCPs) {
|
|
75
|
+
const mcpServers = resolveSelectedMCPServers(selectedMCPs);
|
|
67
76
|
return { mcpServers };
|
|
68
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Merge CHATI-managed servers into an existing Claude project configuration.
|
|
81
|
+
* Unknown user-managed servers and top-level fields are preserved.
|
|
82
|
+
*/
|
|
83
|
+
export function mergeClaudeMCPConfig(existingContent, selectedMCPs) {
|
|
84
|
+
let existing = {};
|
|
85
|
+
if (existingContent?.trim()) {
|
|
86
|
+
existing = JSON.parse(existingContent);
|
|
87
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
88
|
+
throw new Error('Existing .mcp.json must contain a JSON object');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
existing.mcpServers !== undefined
|
|
93
|
+
&& (!existing.mcpServers || typeof existing.mcpServers !== 'object' || Array.isArray(existing.mcpServers))
|
|
94
|
+
) {
|
|
95
|
+
throw new Error('Existing .mcp.json mcpServers must contain a JSON object');
|
|
96
|
+
}
|
|
97
|
+
const mcpServers = { ...(existing.mcpServers || {}) };
|
|
98
|
+
for (const key of Object.keys(MCP_CONFIGS)) delete mcpServers[key];
|
|
99
|
+
Object.assign(mcpServers, resolveSelectedMCPServers(selectedMCPs));
|
|
100
|
+
return { ...existing, mcpServers };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function tomlString(value) {
|
|
104
|
+
return JSON.stringify(String(value));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Generate the shared native TOML block used by Codex and Grok. */
|
|
108
|
+
export function generateManagedMcpToml(selectedMCPs) {
|
|
109
|
+
const servers = resolveSelectedMCPServers(selectedMCPs);
|
|
110
|
+
const lines = [MCP_TOML_BEGIN];
|
|
111
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
112
|
+
lines.push(`[mcp_servers.${name}]`);
|
|
113
|
+
lines.push(`command = ${tomlString(config.command)}`);
|
|
114
|
+
if (config.args?.length) lines.push(`args = [${config.args.map(tomlString).join(', ')}]`);
|
|
115
|
+
if (config.env && Object.keys(config.env).length > 0) {
|
|
116
|
+
const values = Object.entries(config.env).map(([key, value]) => `${key} = ${tomlString(value)}`);
|
|
117
|
+
lines.push(`env = { ${values.join(', ')} }`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
}
|
|
121
|
+
lines.push(MCP_TOML_END);
|
|
122
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Replace only CHATI's marked TOML block and preserve all user configuration. */
|
|
126
|
+
export function upsertManagedMcpToml(existingContent, selectedMCPs) {
|
|
127
|
+
const managedPattern = new RegExp(`(?:^|\\n)${MCP_TOML_BEGIN}[\\s\\S]*?${MCP_TOML_END}\\n?`);
|
|
128
|
+
const withoutManaged = String(existingContent || '').replace(managedPattern, '\n').trimEnd();
|
|
129
|
+
const userManagedNames = new Set();
|
|
130
|
+
const tablePattern = /^\s*\[mcp_servers\.(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))\]\s*(?:#.*)?$/gm;
|
|
131
|
+
for (const match of withoutManaged.matchAll(tablePattern)) {
|
|
132
|
+
userManagedNames.add(match[1] || match[2] || match[3]);
|
|
133
|
+
}
|
|
134
|
+
const managedSelections = (selectedMCPs || []).filter((name) => !userManagedNames.has(name));
|
|
135
|
+
if (!managedSelections.length) return withoutManaged ? `${withoutManaged}\n` : '';
|
|
136
|
+
const separator = withoutManaged ? '\n\n' : '';
|
|
137
|
+
return `${withoutManaged}${separator}${generateManagedMcpToml(managedSelections)}\n`;
|
|
138
|
+
}
|