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.
Files changed (35) hide show
  1. package/dist/agents/canonical-agent-projection.js +18 -0
  2. package/dist/agents/renderers/claude-renderer.js +3 -3
  3. package/dist/cli/cli-flags.js +1 -1
  4. package/dist/cli/cli-help.js +7 -7
  5. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +2 -2
  6. package/dist/cli/commands/mcp-dispatcher.js +11 -1
  7. package/dist/cli/commands/setup-dispatcher.js +9 -0
  8. package/dist/cli/tui/main-menu/main-menu-read-model.js +41 -44
  9. package/dist/cli/tui/main-menu/main-menu-render-shape.js +15 -15
  10. package/dist/cli/tui/opentui/main-menu/screen.js +39 -41
  11. package/dist/cli/tui/opentui/main-menu/smoke.js +1 -1
  12. package/dist/cli/tui/opentui/main-menu/view.js +1 -1
  13. package/dist/cli/tui/setup/setup-tui-read-model.js +15 -12
  14. package/dist/mcp/claude-code-agent-config.js +23 -7
  15. package/dist/mcp/claude-code-cli.js +71 -0
  16. package/dist/mcp/claude-code-config.js +1 -1
  17. package/dist/mcp/claude-code-project-memory.js +127 -0
  18. package/dist/mcp/claude-code-scope.js +18 -0
  19. package/dist/mcp/client-install-claude-code-contract.js +40 -12
  20. package/dist/mcp/client-install-claude-code.js +61 -10
  21. package/dist/mcp/index.js +3 -0
  22. package/dist/mcp/provider-change-plan.js +56 -4
  23. package/dist/mcp/provider-doctor.js +55 -5
  24. package/dist/mcp/provider-health-types.js +4 -0
  25. package/dist/mcp/provider-status.js +84 -8
  26. package/dist/mcp/schema.js +4 -3
  27. package/dist/setup/providers/claude-setup-adapter.js +9 -7
  28. package/dist/setup/setup-plan.js +60 -1
  29. package/docs/architecture.md +2 -2
  30. package/docs/cli.md +37 -2
  31. package/docs/glossary.md +2 -2
  32. package/docs/prd.md +2 -2
  33. package/docs/providers.md +33 -6
  34. package/docs/roadmap.md +1 -1
  35. 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, 'CLAUDE.md'), join(workspaceRoot, '.claude', 'CLAUDE.md')];
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
- return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness), status: 'refused', reason, message };
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
- 'Claude Code support is project-local only and writes only .mcp.json plus .claude/agents/*.md after explicit confirmation.',
67
- 'VGXNESS never writes ~/.claude.json, CLAUDE.md, or .claude/CLAUDE.md and does not execute or install Claude Code.',
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.filter((target) => target.action !== 'blocked').map((target) => target.path));
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
- const existingTargets = plan.targets.filter((target) => target.action === 'merge' || target.action === 'update-vgxness');
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 createBackup(path) {
72
- return createManagedProviderConfigBackup({ targetPath: path, provider: 'claude', scope: 'project', createdByOperation: 'mcp-client-install-claude-code', reason: 'pre-merge-safety', description: 'Backup existing Claude Code project config before merging VGXNESS MCP or agent configuration.' });
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 and ${installPlan.agentNames.length} agent file(s).`,
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')