vgxness 1.5.2 → 1.6.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/agents/canonical-agent-projection.js +18 -0
- package/dist/agents/renderers/claude-renderer.js +3 -3
- package/dist/cli/cli-flags.js +1 -1
- package/dist/cli/cli-help.js +7 -7
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +2 -2
- package/dist/cli/commands/mcp-dispatcher.js +11 -1
- package/dist/cli/commands/setup-dispatcher.js +9 -0
- package/dist/cli/tui/main-menu/main-menu-read-model.js +41 -44
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +15 -15
- package/dist/cli/tui/opentui/main-menu/screen.js +39 -41
- package/dist/cli/tui/opentui/main-menu/smoke.js +1 -1
- package/dist/cli/tui/opentui/main-menu/view.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +15 -12
- package/dist/mcp/claude-code-agent-config.js +23 -7
- package/dist/mcp/claude-code-cli.js +71 -0
- package/dist/mcp/claude-code-config.js +1 -1
- package/dist/mcp/claude-code-project-memory.js +127 -0
- package/dist/mcp/claude-code-scope.js +18 -0
- package/dist/mcp/client-install-claude-code-contract.js +40 -12
- package/dist/mcp/client-install-claude-code.js +61 -10
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/provider-change-plan.js +56 -4
- package/dist/mcp/provider-doctor.js +55 -5
- package/dist/mcp/provider-health-types.js +4 -0
- package/dist/mcp/provider-status.js +84 -8
- package/dist/mcp/schema.js +4 -3
- package/dist/setup/providers/claude-setup-adapter.js +9 -7
- package/dist/setup/setup-plan.js +60 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +37 -2
- package/docs/glossary.md +2 -2
- package/docs/prd.md +2 -2
- package/docs/providers.md +33 -6
- package/docs/roadmap.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
3
|
+
export function buildClaudeCodeMcpAddCommand(input = {}) {
|
|
4
|
+
const scope = resolveClaudeCodeScope(input.scope);
|
|
5
|
+
if (!scope.ok)
|
|
6
|
+
return scope;
|
|
7
|
+
const serverName = input.serverName ?? 'vgxness';
|
|
8
|
+
const command = input.command ?? 'vgxness';
|
|
9
|
+
const args = input.args ?? ['mcp', 'start'];
|
|
10
|
+
return commandEnvelope(['mcp', 'add', '--scope', scope.value.canonical, serverName, '--', command, ...args]);
|
|
11
|
+
}
|
|
12
|
+
export function buildClaudeCodeMcpAddJsonCommand(input) {
|
|
13
|
+
const scope = resolveClaudeCodeScope(input.scope);
|
|
14
|
+
if (!scope.ok)
|
|
15
|
+
return scope;
|
|
16
|
+
return commandEnvelope(['mcp', 'add-json', '--scope', scope.value.canonical, input.serverName ?? 'vgxness', sanitizeJsonArgument(input.json)]);
|
|
17
|
+
}
|
|
18
|
+
export async function runClaudeCodeCliCommand(command, runner = defaultClaudeCodeCliRunner) {
|
|
19
|
+
return runner(command);
|
|
20
|
+
}
|
|
21
|
+
export function createClaudeCodeCliRegistrationPreview(scope) {
|
|
22
|
+
const command = buildClaudeCodeMcpAddCommand({ scope });
|
|
23
|
+
return command.ok ? command.value.preview : ['claude', 'mcp', 'add', '--scope', scope, 'vgxness', '--', 'vgxness', 'mcp', 'start'];
|
|
24
|
+
}
|
|
25
|
+
function commandEnvelope(argv) {
|
|
26
|
+
return { ok: true, value: { executable: 'claude', argv, preview: ['claude', ...argv].map(redactArg), redacted: true } };
|
|
27
|
+
}
|
|
28
|
+
function sanitizeJsonArgument(value) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(value);
|
|
31
|
+
return JSON.stringify(redactUnknown(parsed));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '<redacted-json>';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function redactUnknown(value) {
|
|
38
|
+
if (Array.isArray(value))
|
|
39
|
+
return value.map(redactUnknown);
|
|
40
|
+
if (typeof value !== 'object' || value === null)
|
|
41
|
+
return value;
|
|
42
|
+
const redacted = {};
|
|
43
|
+
for (const [key, nested] of Object.entries(value))
|
|
44
|
+
redacted[key] = /token|secret|password|key/i.test(key) ? '<redacted>' : redactUnknown(nested);
|
|
45
|
+
return redacted;
|
|
46
|
+
}
|
|
47
|
+
function redactArg(value) {
|
|
48
|
+
return /token|secret|password/i.test(value) ? '<redacted>' : value;
|
|
49
|
+
}
|
|
50
|
+
function defaultClaudeCodeCliRunner(command) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const child = spawn(command.executable, command.argv, { shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
53
|
+
let stdout = '';
|
|
54
|
+
let stderr = '';
|
|
55
|
+
child.stdout.on('data', (chunk) => {
|
|
56
|
+
stdout += chunk.toString('utf8');
|
|
57
|
+
});
|
|
58
|
+
child.stderr.on('data', (chunk) => {
|
|
59
|
+
stderr += chunk.toString('utf8');
|
|
60
|
+
});
|
|
61
|
+
child.on('error', (cause) => {
|
|
62
|
+
resolve({ command, exitCode: null, stdout: '', stderr: cause.message });
|
|
63
|
+
});
|
|
64
|
+
child.on('close', (exitCode) => {
|
|
65
|
+
resolve({ command, exitCode, stdout: redactOutput(stdout), stderr: redactOutput(stderr) });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function redactOutput(value) {
|
|
70
|
+
return value.replace(/(token|secret|password)=\S+/gi, '$1=<redacted>');
|
|
71
|
+
}
|
|
@@ -74,7 +74,7 @@ export function assertInsideWorkspace(workspaceRoot, targetPath) {
|
|
|
74
74
|
throw new Error(`Target path is outside workspace root: ${targetPath}`);
|
|
75
75
|
}
|
|
76
76
|
export function claudeAdvisoryPaths(workspaceRoot) {
|
|
77
|
-
return [join(workspaceRoot, '.claude', 'settings.json'), join(workspaceRoot, '.claude', 'settings.local.json'), join(workspaceRoot, '
|
|
77
|
+
return [join(workspaceRoot, '.claude', 'settings.json'), join(workspaceRoot, '.claude', 'settings.local.json'), join(workspaceRoot, '.claude', 'CLAUDE.md')];
|
|
78
78
|
}
|
|
79
79
|
function arraysEqual(left, right) {
|
|
80
80
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { projectCanonicalAgentManifestToClaudeProjectMemory } from '../agents/canonical-agent-projection.js';
|
|
4
|
+
import { assertInsideWorkspace } from './claude-code-config.js';
|
|
5
|
+
export const claudeProjectMemoryBeginMarker = '<!-- BEGIN VGXNESS-MANAGED CLAUDE PROJECT MEMORY -->';
|
|
6
|
+
export const claudeProjectMemoryEndMarker = '<!-- END VGXNESS-MANAGED CLAUDE PROJECT MEMORY -->';
|
|
7
|
+
export function resolveClaudeProjectMemoryPath(workspaceRoot) {
|
|
8
|
+
const target = resolve(workspaceRoot, 'CLAUDE.md');
|
|
9
|
+
assertInsideWorkspace(workspaceRoot, target);
|
|
10
|
+
return target;
|
|
11
|
+
}
|
|
12
|
+
export function renderClaudeProjectMemoryBlock() {
|
|
13
|
+
const projection = projectCanonicalAgentManifestToClaudeProjectMemory();
|
|
14
|
+
return [
|
|
15
|
+
claudeProjectMemoryBeginMarker,
|
|
16
|
+
`<!-- VGXNESS-GENERATED owner=vgxness provider=claude artifact=claude-project-memory safe-update=true defaultAgent=${projection.defaultAgent} promptContractVersion=${projection.promptContractVersion} canonicalSource=canonical-agent-manifest repositoryInstructions=${projection.repositoryInstructions} -->`,
|
|
17
|
+
'',
|
|
18
|
+
'# VGXNESS Claude Project Memory',
|
|
19
|
+
'',
|
|
20
|
+
projection.guidance[0] ?? '',
|
|
21
|
+
'',
|
|
22
|
+
...projection.guidance.slice(1).map((line) => `- ${line}`),
|
|
23
|
+
'',
|
|
24
|
+
claudeProjectMemoryEndMarker,
|
|
25
|
+
].join('\n');
|
|
26
|
+
}
|
|
27
|
+
export function inspectClaudeProjectMemory(workspaceRoot) {
|
|
28
|
+
let path;
|
|
29
|
+
try {
|
|
30
|
+
path = resolveClaudeProjectMemoryPath(workspaceRoot);
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
return blocked(resolve(workspaceRoot, 'CLAUDE.md'), false, 'outside_workspace', cause instanceof Error ? cause.message : String(cause));
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(path))
|
|
36
|
+
return { status: 'missing', path, exists: false, action: 'create', backupRequired: false, message: 'Claude project memory CLAUDE.md does not exist.' };
|
|
37
|
+
let contents;
|
|
38
|
+
try {
|
|
39
|
+
const bytes = readFileSync(path);
|
|
40
|
+
contents = bytes.toString('utf8');
|
|
41
|
+
if (!Buffer.from(contents, 'utf8').equals(bytes))
|
|
42
|
+
return blocked(path, true, 'non_utf8', 'Claude project memory is not valid UTF-8; refusing safe managed-block updates.');
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
return blocked(path, true, 'unreadable', `Claude project memory could not be read: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
46
|
+
}
|
|
47
|
+
const beginCount = countOccurrences(contents, claudeProjectMemoryBeginMarker);
|
|
48
|
+
const endCount = countOccurrences(contents, claudeProjectMemoryEndMarker);
|
|
49
|
+
if (beginCount === 0 && endCount === 0)
|
|
50
|
+
return { status: 'unmanaged', path, exists: true, action: 'append-managed-block', backupRequired: true, message: 'Claude project memory exists without a VGXNESS managed block; future apply would append one after backup.', contents };
|
|
51
|
+
if (beginCount === 0)
|
|
52
|
+
return blocked(path, true, 'missing_begin_marker', 'Claude project memory has an end marker without a begin marker.');
|
|
53
|
+
if (endCount === 0)
|
|
54
|
+
return blocked(path, true, 'missing_end_marker', 'Claude project memory has a begin marker without an end marker.');
|
|
55
|
+
if (beginCount > 1 || endCount > 1)
|
|
56
|
+
return blocked(path, true, 'duplicate_markers', 'Claude project memory has duplicate VGXNESS managed-block markers.');
|
|
57
|
+
const start = contents.indexOf(claudeProjectMemoryBeginMarker);
|
|
58
|
+
const endMarkerStart = contents.indexOf(claudeProjectMemoryEndMarker);
|
|
59
|
+
if (endMarkerStart < start)
|
|
60
|
+
return blocked(path, true, 'reordered_markers', 'Claude project memory end marker appears before begin marker.');
|
|
61
|
+
const end = endMarkerStart + claudeProjectMemoryEndMarker.length;
|
|
62
|
+
const inner = contents.slice(start + claudeProjectMemoryBeginMarker.length, endMarkerStart);
|
|
63
|
+
if (inner.includes(claudeProjectMemoryBeginMarker) || inner.includes(claudeProjectMemoryEndMarker))
|
|
64
|
+
return blocked(path, true, 'nested_markers', 'Claude project memory has nested VGXNESS managed-block markers.');
|
|
65
|
+
const metadata = parseMetadata(inner);
|
|
66
|
+
if (metadata === undefined)
|
|
67
|
+
return blocked(path, true, 'invalid_metadata', 'Claude project memory managed block is missing VGXNESS metadata.');
|
|
68
|
+
const ownership = validateMetadataOwnership(metadata);
|
|
69
|
+
if (!ownership.ok)
|
|
70
|
+
return blocked(path, true, ownership.reason, ownership.message);
|
|
71
|
+
const expected = renderClaudeProjectMemoryBlock();
|
|
72
|
+
const actual = contents.slice(start, end);
|
|
73
|
+
if (actual === expected)
|
|
74
|
+
return { status: 'managed-current', path, exists: true, action: 'none', backupRequired: false, message: 'Claude project memory has the current VGXNESS managed block.', blockRange: { start, end }, contents };
|
|
75
|
+
return { status: 'managed-stale', path, exists: true, action: 'update-managed-block', backupRequired: true, message: 'Claude project memory has a stale VGXNESS managed block; future apply would update only that block after backup.', blockRange: { start, end }, staleReasons: ['managed_block_differs_from_canonical_render'], contents };
|
|
76
|
+
}
|
|
77
|
+
export function mergeClaudeProjectMemory(state) {
|
|
78
|
+
const block = renderClaudeProjectMemoryBlock();
|
|
79
|
+
if (state.status === 'missing')
|
|
80
|
+
return { ok: true, value: { path: state.path, contents: `${block}\n` } };
|
|
81
|
+
if (state.status === 'unmanaged') {
|
|
82
|
+
const separator = state.contents.length === 0 ? '' : state.contents.endsWith('\n') ? '\n' : '\n\n';
|
|
83
|
+
return { ok: true, value: { path: state.path, contents: `${state.contents}${separator}${block}\n` } };
|
|
84
|
+
}
|
|
85
|
+
if (state.status === 'managed-stale')
|
|
86
|
+
return { ok: true, value: { path: state.path, contents: state.contents.slice(0, state.blockRange.start) + block + state.contents.slice(state.blockRange.end) } };
|
|
87
|
+
if (state.status === 'managed-current')
|
|
88
|
+
return { ok: true, value: { path: state.path, contents: state.contents } };
|
|
89
|
+
return { ok: false, error: { code: 'validation_failed', message: state.message } };
|
|
90
|
+
}
|
|
91
|
+
function blocked(path, exists, reason, message) {
|
|
92
|
+
return { status: 'blocked', path, exists, action: 'blocked', backupRequired: false, reason, message };
|
|
93
|
+
}
|
|
94
|
+
function countOccurrences(contents, needle) {
|
|
95
|
+
let count = 0;
|
|
96
|
+
let index = contents.indexOf(needle);
|
|
97
|
+
while (index !== -1) {
|
|
98
|
+
count += 1;
|
|
99
|
+
index = contents.indexOf(needle, index + needle.length);
|
|
100
|
+
}
|
|
101
|
+
return count;
|
|
102
|
+
}
|
|
103
|
+
function parseMetadata(inner) {
|
|
104
|
+
const line = inner.split(/\r?\n/).find((item) => item.includes('VGXNESS-GENERATED'));
|
|
105
|
+
if (line === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const metadata = {};
|
|
108
|
+
for (const part of line.replace('<!--', '').replace('-->', '').trim().split(/\s+/)) {
|
|
109
|
+
const [key, value] = part.split('=');
|
|
110
|
+
if (key !== undefined && value !== undefined)
|
|
111
|
+
metadata[key] = value;
|
|
112
|
+
}
|
|
113
|
+
return metadata;
|
|
114
|
+
}
|
|
115
|
+
function validateMetadataOwnership(metadata) {
|
|
116
|
+
if (metadata['VGXNESS-GENERATED'] !== undefined)
|
|
117
|
+
return { ok: false, reason: 'invalid_metadata', message: 'Claude project memory metadata is malformed.' };
|
|
118
|
+
if (metadata.owner !== 'vgxness')
|
|
119
|
+
return { ok: false, reason: 'conflicting_ownership', message: 'Claude project memory managed block is not owned by VGXNESS.' };
|
|
120
|
+
if (metadata.provider !== 'claude')
|
|
121
|
+
return { ok: false, reason: 'conflicting_ownership', message: 'Claude project memory managed block is not for the Claude provider.' };
|
|
122
|
+
if (metadata.artifact !== 'claude-project-memory')
|
|
123
|
+
return { ok: false, reason: 'conflicting_ownership', message: 'Claude project memory managed block has a conflicting artifact type.' };
|
|
124
|
+
if (metadata['safe-update'] !== 'true')
|
|
125
|
+
return { ok: false, reason: 'conflicting_ownership', message: 'Claude project memory managed block is not marked safe for update.' };
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const claudeCodeCanonicalScopes = ['local', 'project', 'user'];
|
|
2
|
+
export function resolveClaudeCodeScope(input, fallback = 'project') {
|
|
3
|
+
if (input !== undefined && input.trim().length === 0)
|
|
4
|
+
return { ok: false, error: { code: 'validation_failed', message: '--scope for Claude must be local, project, or user (compatibility aliases: personal, global).' } };
|
|
5
|
+
const raw = input?.trim() || fallback;
|
|
6
|
+
if (raw === 'local' || raw === 'project' || raw === 'user')
|
|
7
|
+
return { ok: true, value: { canonical: raw, input: raw, warnings: [] } };
|
|
8
|
+
if (raw === 'personal') {
|
|
9
|
+
return { ok: true, value: { canonical: 'user', input: raw, warnings: ['Claude Code scope `personal` is accepted for VGXNESS compatibility and maps to Claude `user`.'] } };
|
|
10
|
+
}
|
|
11
|
+
if (raw === 'global') {
|
|
12
|
+
return { ok: true, value: { canonical: 'user', input: raw, warnings: ['Claude Code legacy scope `global` is deprecated and maps to Claude `user`.'] } };
|
|
13
|
+
}
|
|
14
|
+
return { ok: false, error: { code: 'validation_failed', message: '--scope for Claude must be local, project, or user (compatibility aliases: personal, global).' } };
|
|
15
|
+
}
|
|
16
|
+
export function isClaudeCodeUserScope(scope) {
|
|
17
|
+
return scope === 'user';
|
|
18
|
+
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { expectedClaudeCodeAgentFiles, inspectClaudeCodeAgents, renderClaudeCodeAgentMarkdown } from './claude-code-agent-config.js';
|
|
2
|
+
import { buildClaudeCodeMcpAddCommand } from './claude-code-cli.js';
|
|
2
3
|
import { createClaudeCodeMcpDoctorCommand, createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, resolveClaudeCodeMcpJsonPath } from './claude-code-config.js';
|
|
4
|
+
import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
5
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
3
6
|
export function planClaudeCodeMcpInstall(input) {
|
|
4
7
|
const source = input.databasePathSource ?? 'flag';
|
|
5
8
|
const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
|
|
6
9
|
const overwriteVgxness = input.overwriteVgxness === true;
|
|
10
|
+
const resolvedScope = resolveClaudeCodeScope(input.scope);
|
|
11
|
+
if (!resolvedScope.ok)
|
|
12
|
+
return refused(input, server, 'unsupported_scope', resolvedScope.error.message, [], [], overwriteVgxness);
|
|
13
|
+
const cliCommand = buildClaudeCodeMcpAddCommand({ scope: resolvedScope.value.canonical });
|
|
14
|
+
if (!cliCommand.ok)
|
|
15
|
+
return refused(input, server, 'unsupported_scope', cliCommand.error.message, [], [], overwriteVgxness);
|
|
16
|
+
if (resolvedScope.value.canonical !== 'project') {
|
|
17
|
+
const targets = [{ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' }];
|
|
18
|
+
return { ...base(input, server, targets, [], false, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings), status: 'would_install' };
|
|
19
|
+
}
|
|
7
20
|
let mcpPath;
|
|
8
21
|
try {
|
|
9
22
|
mcpPath = resolveClaudeCodeMcpJsonPath(input.cwd);
|
|
@@ -13,6 +26,7 @@ export function planClaudeCodeMcpInstall(input) {
|
|
|
13
26
|
}
|
|
14
27
|
const mcpState = inspectClaudeCodeMcpConfig(input.cwd);
|
|
15
28
|
const targets = [];
|
|
29
|
+
targets.push({ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' });
|
|
16
30
|
const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
|
|
17
31
|
if (mcpState.status === 'missing')
|
|
18
32
|
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'create' });
|
|
@@ -25,20 +39,22 @@ export function planClaudeCodeMcpInstall(input) {
|
|
|
25
39
|
const agentInspection = inspectClaudeCodeAgents(input.cwd);
|
|
26
40
|
for (const agent of agentInspection.agents) {
|
|
27
41
|
if (agent.status === 'missing')
|
|
28
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'create' });
|
|
42
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'create' });
|
|
29
43
|
else if (agent.status === 'managed')
|
|
30
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'update-vgxness' });
|
|
44
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'update-vgxness' });
|
|
31
45
|
else
|
|
32
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'blocked', reason: agent.detail });
|
|
46
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'blocked', reason: agent.detail });
|
|
33
47
|
}
|
|
48
|
+
const projectMemory = inspectClaudeProjectMemory(input.cwd);
|
|
49
|
+
targets.push({ kind: 'project-memory', path: projectMemory.path, action: projectMemory.action, status: projectMemory.status, backupRequired: projectMemory.backupRequired, ...(projectMemory.status === 'blocked' ? { reason: projectMemory.message } : {}) });
|
|
34
50
|
const blocked = targets.find((target) => target.action === 'blocked');
|
|
35
51
|
if (blocked !== undefined) {
|
|
36
|
-
const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : 'existing_vgxness_agent';
|
|
52
|
+
const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : blocked.kind === 'project-memory' ? projectMemoryRefusalReason(projectMemory) : 'existing_vgxness_agent';
|
|
37
53
|
return refused(input, server, reason, blocked.reason ?? 'Claude Code install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
|
|
38
54
|
}
|
|
39
|
-
const backupRequired = targets.some((target) => (target.kind === 'mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness'));
|
|
55
|
+
const backupRequired = targets.some((target) => (target.kind === 'mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness') || (target.kind === 'project-memory' && target.backupRequired));
|
|
40
56
|
return {
|
|
41
|
-
...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness),
|
|
57
|
+
...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings),
|
|
42
58
|
status: 'would_install',
|
|
43
59
|
};
|
|
44
60
|
}
|
|
@@ -46,25 +62,30 @@ export function expectedClaudeCodeRenderedAgents(workspaceRoot) {
|
|
|
46
62
|
return expectedClaudeCodeAgentFiles(workspaceRoot).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
|
|
47
63
|
}
|
|
48
64
|
function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
|
|
49
|
-
|
|
65
|
+
const resolved = resolveClaudeCodeScope(input.scope);
|
|
66
|
+
const canonical = resolved.ok ? resolved.value.canonical : 'project';
|
|
67
|
+
const cli = buildClaudeCodeMcpAddCommand({ scope: canonical });
|
|
68
|
+
return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness, canonical, cli.ok ? cli.value : undefined, resolved.ok ? resolved.value.warnings : []), status: 'refused', reason, message };
|
|
50
69
|
}
|
|
51
|
-
function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness) {
|
|
70
|
+
function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, canonicalClaudeScope, cliCommand, scopeWarnings) {
|
|
52
71
|
const source = input.databasePathSource ?? 'flag';
|
|
53
|
-
const targetPath = resolveClaudeCodeMcpJsonPath(input.cwd)
|
|
72
|
+
const targetPath = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : `claude-cli:${canonicalClaudeScope}:vgxness`;
|
|
54
73
|
return {
|
|
55
74
|
version: 1,
|
|
56
75
|
kind: 'mcp-client-install-claude-code',
|
|
57
76
|
installable: true,
|
|
58
77
|
mutating: false,
|
|
59
78
|
provider: 'claude',
|
|
60
|
-
scope: 'project',
|
|
79
|
+
scope: input.scope ?? 'project',
|
|
61
80
|
targetPath,
|
|
62
81
|
targets,
|
|
63
82
|
backupRequired,
|
|
64
83
|
safety: { operation: 'plan', mutating: false, writesProviderConfig: false, targetPath, backupRequired, mergePolicy: backupRequired ? 'merge-preserve-existing' : 'create' },
|
|
65
84
|
warnings: [
|
|
66
|
-
|
|
67
|
-
'
|
|
85
|
+
...scopeWarnings,
|
|
86
|
+
'Claude Code MCP registration is modeled as Claude CLI argv, never shell strings or manual ~/.claude.json mutation.',
|
|
87
|
+
'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight; user/local scopes do not inspect or write private Claude config.',
|
|
88
|
+
'VGXNESS never manually writes ~/.claude.json or .claude/CLAUDE.md; project-root CLAUDE.md is only touched through the guarded project install managed block.',
|
|
68
89
|
],
|
|
69
90
|
verificationHints: [
|
|
70
91
|
{ kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed project config installation.' },
|
|
@@ -72,6 +93,8 @@ function base(input, server, targets, preservedTopLevelKeys, backupRequired, ove
|
|
|
72
93
|
{ kind: 'command', message: 'Run the MCP doctor command after installation.', command: createClaudeCodeMcpDoctorCommand(input.databasePath, source) },
|
|
73
94
|
],
|
|
74
95
|
server,
|
|
96
|
+
...(cliCommand === undefined ? {} : { cliCommand }),
|
|
97
|
+
canonicalClaudeScope,
|
|
75
98
|
preservedTopLevelKeys,
|
|
76
99
|
agentNames: targets.filter((target) => target.kind === 'agent-file').map((target) => target.agentName),
|
|
77
100
|
overwriteVgxness,
|
|
@@ -84,3 +107,8 @@ function mcpRefusalReason(status) {
|
|
|
84
107
|
return 'existing_vgxness_mcp';
|
|
85
108
|
return 'invalid_mcp_shape';
|
|
86
109
|
}
|
|
110
|
+
function projectMemoryRefusalReason(state) {
|
|
111
|
+
if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
|
|
112
|
+
return 'conflicting_claude_project_memory';
|
|
113
|
+
return 'malformed_claude_project_memory';
|
|
114
|
+
}
|
|
@@ -2,20 +2,24 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createManagedProviderConfigBackup } from '../setup/backup-rollback-service.js';
|
|
4
4
|
import { parseClaudeAgentFrontmatter } from './claude-code-agent-config.js';
|
|
5
|
+
import { runClaudeCodeCliCommand } from './claude-code-cli.js';
|
|
5
6
|
import { createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, mergeClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
7
|
+
import { inspectClaudeProjectMemory, mergeClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
6
8
|
import { expectedClaudeCodeRenderedAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
|
|
7
9
|
export async function installClaudeCodeMcpClient(input) {
|
|
8
10
|
const source = input.databasePathSource ?? 'flag';
|
|
9
11
|
const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
|
|
10
|
-
const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}) });
|
|
11
|
-
if (!input.confirmed)
|
|
12
|
-
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
|
|
12
|
+
const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}), ...(input.scope !== undefined ? { scope: input.scope } : {}) });
|
|
13
13
|
if (plan.status === 'refused')
|
|
14
14
|
return refusal(plan.reason, plan.message, plan, server, [], []);
|
|
15
|
+
if (!input.confirmed)
|
|
16
|
+
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
|
|
15
17
|
if (input.preflight === undefined) {
|
|
16
18
|
return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any project config write.', plan, server, [], []);
|
|
17
19
|
}
|
|
18
|
-
const preflightPaths = unique(plan.targets.
|
|
20
|
+
const preflightPaths = unique(plan.targets.flatMap((target) => (isMutatingTarget(target) ? [target.path] : [])));
|
|
21
|
+
if (plan.canonicalClaudeScope !== 'project')
|
|
22
|
+
preflightPaths.unshift(plan.targetPath);
|
|
19
23
|
for (const targetPath of preflightPaths) {
|
|
20
24
|
const preflight = await input.preflight({
|
|
21
25
|
category: 'provider-tool',
|
|
@@ -32,9 +36,20 @@ export async function installClaudeCodeMcpClient(input) {
|
|
|
32
36
|
}
|
|
33
37
|
const backups = [];
|
|
34
38
|
const writtenPaths = [];
|
|
35
|
-
|
|
39
|
+
if (plan.canonicalClaudeScope !== 'project') {
|
|
40
|
+
if (input.cliRunner === undefined)
|
|
41
|
+
return refusal('preflight_failed', 'Claude CLI MCP registration apply requires an injected runner boundary; read-only plans never execute Claude Code.', plan, server, [], []);
|
|
42
|
+
if (plan.cliCommand === undefined)
|
|
43
|
+
return refusal('preflight_failed', 'Claude CLI MCP registration command is unavailable.', plan, server, [], []);
|
|
44
|
+
const cli = await runClaudeCodeCliCommand(plan.cliCommand, input.cliRunner);
|
|
45
|
+
const cliResult = { exitCode: cli.exitCode, stdout: cli.stdout, stderr: cli.stderr, preview: cli.command.preview };
|
|
46
|
+
if (cli.exitCode !== 0)
|
|
47
|
+
return refusal('preflight_failed', `Claude CLI MCP registration failed with exit code ${cli.exitCode ?? 'unknown'}.`, plan, server, [], [], cliResult);
|
|
48
|
+
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths: [], backups: [], safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness, cliResult };
|
|
49
|
+
}
|
|
50
|
+
const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block'));
|
|
36
51
|
for (const target of existingTargets) {
|
|
37
|
-
const backup = createBackup(target.path);
|
|
52
|
+
const backup = createBackup(target.path, target.kind === 'project-memory' ? 'project-memory' : 'config');
|
|
38
53
|
if (!backup.ok)
|
|
39
54
|
return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
|
|
40
55
|
backups.push(toBackupSummary(backup.value));
|
|
@@ -54,6 +69,11 @@ export async function installClaudeCodeMcpClient(input) {
|
|
|
54
69
|
return refusal('post_write_validation_failed', `Claude agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
|
|
55
70
|
writtenPaths.push(agent.path);
|
|
56
71
|
}
|
|
72
|
+
const projectMemoryWrite = writeProjectMemory(plan);
|
|
73
|
+
if (!projectMemoryWrite.ok)
|
|
74
|
+
return refusal('post_write_validation_failed', projectMemoryWrite.error.message, plan, server, writtenPaths, backups);
|
|
75
|
+
if (projectMemoryWrite.value !== undefined)
|
|
76
|
+
writtenPaths.push(projectMemoryWrite.value);
|
|
57
77
|
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths, backups, safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
|
|
58
78
|
}
|
|
59
79
|
function writeMcpJson(cwd, plan, server) {
|
|
@@ -68,11 +88,35 @@ function writeMcpJson(cwd, plan, server) {
|
|
|
68
88
|
const after = inspectClaudeCodeMcpConfig(cwd);
|
|
69
89
|
return after.status === 'configured' ? { ok: true, value: plan.targetPath } : { ok: false, error: { code: 'validation_failed', message: 'Claude .mcp.json did not validate after write.' } };
|
|
70
90
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
91
|
+
function writeProjectMemory(plan) {
|
|
92
|
+
const target = plan.targets.find((item) => item.kind === 'project-memory');
|
|
93
|
+
if (target === undefined || target.action === 'none')
|
|
94
|
+
return { ok: true, value: undefined };
|
|
95
|
+
if (target.action === 'blocked')
|
|
96
|
+
return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude project memory is blocked.' } };
|
|
97
|
+
const state = inspectClaudeProjectMemory(dirname(target.path));
|
|
98
|
+
if (state.action !== target.action)
|
|
99
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude project memory changed after planning; rerun apply.' } };
|
|
100
|
+
const merged = mergeClaudeProjectMemory(state);
|
|
101
|
+
if (!merged.ok)
|
|
102
|
+
return merged;
|
|
103
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
104
|
+
writeFileSync(target.path, merged.value.contents);
|
|
105
|
+
const after = inspectClaudeProjectMemory(dirname(target.path));
|
|
106
|
+
return after.status === 'managed-current' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude project memory did not validate as managed-current after write.' } };
|
|
107
|
+
}
|
|
108
|
+
function createBackup(path, kind) {
|
|
109
|
+
return createManagedProviderConfigBackup({
|
|
110
|
+
targetPath: path,
|
|
111
|
+
provider: 'claude',
|
|
112
|
+
scope: 'project',
|
|
113
|
+
createdByOperation: 'mcp-client-install-claude-code',
|
|
114
|
+
reason: kind === 'project-memory' ? 'pre-project-memory-update-safety' : 'pre-merge-safety',
|
|
115
|
+
description: kind === 'project-memory' ? 'Backup existing Claude Code project memory before appending or updating the VGXNESS managed block.' : 'Backup existing Claude Code project config before merging VGXNESS MCP or agent configuration.',
|
|
116
|
+
});
|
|
73
117
|
}
|
|
74
|
-
function refusal(reason, message, plan, server, writtenPaths, backups) {
|
|
75
|
-
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'refused', reason, message, targetPath: plan.targetPath, writtenPaths, backups, safety: { ...plan.safety, operation: 'apply', mutating: false, writesProviderConfig: false }, server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
|
|
118
|
+
function refusal(reason, message, plan, server, writtenPaths, backups, cliResult) {
|
|
119
|
+
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'refused', reason, message, targetPath: plan.targetPath, writtenPaths, backups, safety: { ...plan.safety, operation: 'apply', mutating: false, writesProviderConfig: false }, server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness, ...(cliResult === undefined ? {} : { cliResult }) };
|
|
76
120
|
}
|
|
77
121
|
function applySafety(plan) {
|
|
78
122
|
return { ...plan.safety, operation: 'apply', mutating: true, writesProviderConfig: true };
|
|
@@ -83,3 +127,10 @@ function toBackupSummary(backup) {
|
|
|
83
127
|
function unique(values) {
|
|
84
128
|
return [...new Set(values)];
|
|
85
129
|
}
|
|
130
|
+
function isMutatingTarget(target) {
|
|
131
|
+
if (target.kind === 'cli-mcp-registration')
|
|
132
|
+
return false;
|
|
133
|
+
if (target.action === 'blocked' || target.action === 'none')
|
|
134
|
+
return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -6,7 +6,10 @@ export * from './client-install-claude-code.js';
|
|
|
6
6
|
export * from './client-install-claude-code-contract.js';
|
|
7
7
|
export * from './client-setup-preview.js';
|
|
8
8
|
export * from './claude-code-agent-config.js';
|
|
9
|
+
export * from './claude-code-cli.js';
|
|
9
10
|
export * from './claude-code-config.js';
|
|
11
|
+
export * from './claude-code-project-memory.js';
|
|
12
|
+
export * from './claude-code-scope.js';
|
|
10
13
|
export * from './control-plane.js';
|
|
11
14
|
export * from './doctor.js';
|
|
12
15
|
export * from './opencode-visibility.js';
|
|
@@ -2,7 +2,10 @@ import { resolveMemoryDatabasePath } from '../memory/storage-paths.js';
|
|
|
2
2
|
import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
|
|
3
3
|
import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
|
|
4
4
|
import { ProviderDoctorService } from './provider-doctor.js';
|
|
5
|
+
import { CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, isUserGlobalScope } from './provider-health-types.js';
|
|
5
6
|
import { ProviderStatusService } from './provider-status.js';
|
|
7
|
+
import { createClaudeCodeCliRegistrationPreview } from './claude-code-cli.js';
|
|
8
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
6
9
|
export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
7
10
|
export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
8
11
|
const safety = {
|
|
@@ -75,6 +78,11 @@ export class ProviderChangePlanService {
|
|
|
75
78
|
const doctor = (this.deps.providerDoctor ?? new ProviderDoctorService()).getDoctor({ project: normalized.project, scope: normalized.scope, providerAdapter: 'claude', workspaceRoot: normalized.workspaceRoot, env: this.deps.env ?? process.env, payloadMode: normalized.payloadMode });
|
|
76
79
|
if (!doctor.ok)
|
|
77
80
|
return doctor;
|
|
81
|
+
const claudeScope = resolveClaudeCodeScope(normalized.scope);
|
|
82
|
+
if (!claudeScope.ok)
|
|
83
|
+
return claudeScope;
|
|
84
|
+
if (isUserGlobalScope(normalized.scope) || claudeScope.value.canonical === 'local')
|
|
85
|
+
return { ok: true, value: claudeUserGlobalEnvelope(normalized, status.value, doctor.value, claudeScope.value.canonical, claudeScope.value.warnings) };
|
|
78
86
|
const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
|
|
79
87
|
if (!databasePath.ok)
|
|
80
88
|
return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
|
|
@@ -82,6 +90,42 @@ export class ProviderChangePlanService {
|
|
|
82
90
|
return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
|
|
83
91
|
}
|
|
84
92
|
}
|
|
93
|
+
function claudeUserGlobalEnvelope(input, status, doctor, canonicalScope = 'user', scopeWarnings = []) {
|
|
94
|
+
const warnings = [
|
|
95
|
+
...scopeWarnings,
|
|
96
|
+
...statusWarnings(status),
|
|
97
|
+
...doctor.recommendations,
|
|
98
|
+
'Claude private config mutation is intentionally unsupported by VGXNESS; future apply uses Claude CLI argv only after confirmation/preflight.',
|
|
99
|
+
];
|
|
100
|
+
const envelope = {
|
|
101
|
+
...baseEnvelope(input),
|
|
102
|
+
supported: true,
|
|
103
|
+
status: 'planned',
|
|
104
|
+
summary: `Read-only Claude ${canonicalScope} change planning completed. VGXNESS will not read or write private Claude config files from this plan. Future apply requires confirmation/preflight and uses Claude CLI argv only: claude mcp add --scope ${canonicalScope} vgxness -- vgxness mcp start.`,
|
|
105
|
+
providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
|
|
106
|
+
statusSummary: statusSummary(status),
|
|
107
|
+
statusFindings: status.config,
|
|
108
|
+
doctorSummary: doctorSummary(doctor),
|
|
109
|
+
doctorFindings: doctor.checks,
|
|
110
|
+
previewEffects: {
|
|
111
|
+
action: 'none',
|
|
112
|
+
backupRequired: false,
|
|
113
|
+
preservedTopLevelKeys: [],
|
|
114
|
+
cliCommandPreview: createClaudeCodeCliRegistrationPreview(canonicalScope),
|
|
115
|
+
installsAgents: false,
|
|
116
|
+
agentNames: [],
|
|
117
|
+
refusalMessage: 'Read-only plan only; future apply requires confirmation/preflight and does not mutate ~/.claude.json manually.',
|
|
118
|
+
},
|
|
119
|
+
backupRollback: descriptiveBackupPolicy(false),
|
|
120
|
+
confirmations: confirmationPolicy(),
|
|
121
|
+
risks: [
|
|
122
|
+
`VGXNESS cannot verify Claude ${canonicalScope} MCP registration from read-only planning because it intentionally avoids reading private Claude config and does not execute Claude Code.`,
|
|
123
|
+
`Supported Claude read-only capabilities are: ${CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES.join(', ')}.`,
|
|
124
|
+
],
|
|
125
|
+
warnings,
|
|
126
|
+
};
|
|
127
|
+
return input.payloadMode === 'verbose' ? { ...envelope, rawSources: { status, doctor } } : envelope;
|
|
128
|
+
}
|
|
85
129
|
function normalizeInput(input) {
|
|
86
130
|
return {
|
|
87
131
|
project: input.project?.trim() || 'vgxness',
|
|
@@ -180,7 +224,7 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
|
|
|
180
224
|
supported: true,
|
|
181
225
|
status: installPlan.status === 'refused' ? 'blocked' : 'planned',
|
|
182
226
|
...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
|
|
183
|
-
summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update project .mcp.json
|
|
227
|
+
summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update project .mcp.json, ${installPlan.agentNames.length} agent file(s), and project-root CLAUDE.md as needed.`,
|
|
184
228
|
providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
|
|
185
229
|
statusSummary: statusSummary(status),
|
|
186
230
|
statusFindings: status.config,
|
|
@@ -262,10 +306,18 @@ function previewEffects(plan) {
|
|
|
262
306
|
};
|
|
263
307
|
}
|
|
264
308
|
function previewEffectsClaude(plan) {
|
|
309
|
+
const projectMemory = projectMemoryPreview(plan.targets);
|
|
265
310
|
if (plan.status === 'refused')
|
|
266
|
-
return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message };
|
|
311
|
+
return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message, ...(projectMemory === undefined ? {} : { projectMemory }) };
|
|
267
312
|
const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json');
|
|
268
|
-
return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install' };
|
|
313
|
+
return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install', ...(projectMemory === undefined ? {} : { projectMemory }) };
|
|
314
|
+
}
|
|
315
|
+
function projectMemoryPreview(targets) {
|
|
316
|
+
const target = targets.find((item) => item.kind === 'project-memory');
|
|
317
|
+
if (target === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
const action = target.action === 'create' ? 'would-create' : target.action === 'append-managed-block' ? 'would-append' : target.action === 'update-managed-block' ? 'would-update-managed-block' : target.action === 'none' ? 'up-to-date' : 'refused';
|
|
320
|
+
return { path: target.path, status: target.status, action, backupRequired: target.backupRequired, ...(target.reason === undefined ? {} : { message: target.reason }) };
|
|
269
321
|
}
|
|
270
322
|
function descriptiveBackupPolicy(backupRequired) {
|
|
271
323
|
return {
|
|
@@ -300,7 +352,7 @@ function risksForOpenCode(status, doctor, plan, source) {
|
|
|
300
352
|
return risks;
|
|
301
353
|
}
|
|
302
354
|
function risksForClaude(status, doctor, plan, source) {
|
|
303
|
-
const risks = ['Claude project .mcp.json may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
|
|
355
|
+
const risks = ['Claude project .mcp.json and project-root CLAUDE.md may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
|
|
304
356
|
if (status.status !== 'ready')
|
|
305
357
|
risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
|
|
306
358
|
if (doctor.status !== 'healthy')
|