vgxness 1.5.2 → 1.7.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 (40) 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/claude-code-user-config.js +55 -0
  20. package/dist/mcp/claude-code-user-memory.js +90 -0
  21. package/dist/mcp/client-install-claude-code-contract.js +91 -12
  22. package/dist/mcp/client-install-claude-code.js +133 -12
  23. package/dist/mcp/control-plane.js +18 -1
  24. package/dist/mcp/index.js +5 -0
  25. package/dist/mcp/provider-change-plan.js +18 -6
  26. package/dist/mcp/provider-doctor.js +71 -5
  27. package/dist/mcp/provider-health-types.js +4 -0
  28. package/dist/mcp/provider-status.js +77 -8
  29. package/dist/mcp/schema.js +4 -3
  30. package/dist/sdd/schema.js +15 -0
  31. package/dist/sdd/sdd-workflow-service.js +59 -29
  32. package/dist/setup/providers/claude-setup-adapter.js +11 -7
  33. package/dist/setup/setup-plan.js +60 -1
  34. package/docs/architecture.md +2 -2
  35. package/docs/cli.md +37 -2
  36. package/docs/glossary.md +2 -2
  37. package/docs/prd.md +2 -2
  38. package/docs/providers.md +33 -6
  39. package/docs/roadmap.md +1 -1
  40. 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
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isManagedClaudeCodeMcpServer } from './claude-code-config.js';
4
+ import { safeHomeDirectory } from './claude-code-agent-config.js';
5
+ export function resolveClaudeCodeUserMcpJsonPath(env = process.env) {
6
+ return join(safeHomeDirectory(env), '.claude.json');
7
+ }
8
+ export function inspectClaudeCodeUserMcpConfig(env = process.env) {
9
+ const path = resolveClaudeCodeUserMcpJsonPath(env);
10
+ if (!existsSync(path))
11
+ return { status: 'missing', path, exists: false, parsed: false, message: 'Claude user ~/.claude.json does not exist.' };
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
14
+ if (!isRecord(parsed))
15
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude user ~/.claude.json must be a JSON object.' };
16
+ if (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))
17
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude user ~/.claude.json mcpServers must be a JSON object.' };
18
+ const entry = isRecord(parsed.mcpServers) ? parsed.mcpServers.vgxness : undefined;
19
+ if (entry === undefined)
20
+ return { status: 'stale', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json is readable but mcpServers.vgxness is missing.' };
21
+ if (isManagedClaudeCodeMcpServer(entry))
22
+ return { status: 'configured', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json has a managed mcpServers.vgxness entry.' };
23
+ return { status: 'conflicting', path, exists: true, parsed: true, config: parsed, message: 'Claude user ~/.claude.json has a conflicting mcpServers.vgxness entry.' };
24
+ }
25
+ catch (cause) {
26
+ const message = cause instanceof Error ? cause.message : String(cause);
27
+ return { status: 'invalid', path, exists: true, parsed: false, message: `Claude user ~/.claude.json could not be read or parsed: ${message}` };
28
+ }
29
+ }
30
+ export function mergeClaudeCodeUserMcpConfig(existing, server) {
31
+ return { ...existing, mcpServers: { ...(isRecord(existing.mcpServers) ? existing.mcpServers : {}), vgxness: server } };
32
+ }
33
+ export function claudeUserMcpConfigPathStatus(state) {
34
+ return {
35
+ label: 'user ~/.claude.json',
36
+ path: state.path,
37
+ exists: state.exists,
38
+ readable: state.status !== 'invalid',
39
+ parsed: state.parsed,
40
+ status: state.status === 'configured' ? 'pass' : state.status === 'invalid' || state.status === 'conflicting' ? 'fail' : 'not-configured',
41
+ detail: state.message,
42
+ };
43
+ }
44
+ export function claudeUserMcpEntryStatus(state) {
45
+ if (state.status === 'configured')
46
+ return { configured: true, status: 'pass', serverName: 'vgxness', enabled: true, detail: state.message };
47
+ if (state.status === 'conflicting')
48
+ return { configured: true, status: 'fail', serverName: 'vgxness', detail: state.message };
49
+ if (state.status === 'invalid')
50
+ return { configured: false, status: 'fail', serverName: 'vgxness', detail: state.message };
51
+ return { configured: false, status: 'not-configured', serverName: 'vgxness', detail: state.message };
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { projectCanonicalAgentManifestToClaudeProjectMemory } from '../agents/canonical-agent-projection.js';
4
+ import { safeHomeDirectory } from './claude-code-agent-config.js';
5
+ export const claudeUserMemoryBeginMarker = '<!-- BEGIN VGXNESS-MANAGED CLAUDE USER MEMORY -->';
6
+ export const claudeUserMemoryEndMarker = '<!-- END VGXNESS-MANAGED CLAUDE USER MEMORY -->';
7
+ export function resolveClaudeUserMemoryPath(env = process.env) {
8
+ return join(safeHomeDirectory(env), '.claude', 'CLAUDE.md');
9
+ }
10
+ export function renderClaudeUserMemoryBlock() {
11
+ const projection = projectCanonicalAgentManifestToClaudeProjectMemory();
12
+ return [
13
+ claudeUserMemoryBeginMarker,
14
+ `<!-- VGXNESS-GENERATED owner=vgxness provider=claude artifact=claude-user-memory safe-update=true defaultAgent=${projection.defaultAgent} promptContractVersion=${projection.promptContractVersion} canonicalSource=canonical-agent-manifest repositoryInstructions=${projection.repositoryInstructions} -->`,
15
+ '',
16
+ '# VGXNESS Claude User Memory',
17
+ '',
18
+ 'Global/user Claude Code guidance installed by VGXNESS. Use the VGXNESS MCP server for SDD, runs, memory, provider status, and governance-aware workflow progress.',
19
+ '',
20
+ ...projection.guidance.slice(1).map((line) => `- ${line}`),
21
+ '',
22
+ claudeUserMemoryEndMarker,
23
+ ].join('\n');
24
+ }
25
+ export function inspectClaudeUserMemory(env = process.env) {
26
+ const path = resolveClaudeUserMemoryPath(env);
27
+ if (!existsSync(path))
28
+ return { status: 'missing', path, exists: false, action: 'create', backupRequired: false, message: 'Claude user memory ~/.claude/CLAUDE.md does not exist.' };
29
+ let contents;
30
+ try {
31
+ const bytes = readFileSync(path);
32
+ contents = bytes.toString('utf8');
33
+ if (!Buffer.from(contents, 'utf8').equals(bytes))
34
+ return blocked(path, true, 'non_utf8', 'Claude user memory is not valid UTF-8; refusing safe managed-block updates.');
35
+ }
36
+ catch (cause) {
37
+ return blocked(path, true, 'unreadable', `Claude user memory could not be read: ${cause instanceof Error ? cause.message : String(cause)}`);
38
+ }
39
+ const beginCount = countOccurrences(contents, claudeUserMemoryBeginMarker);
40
+ const endCount = countOccurrences(contents, claudeUserMemoryEndMarker);
41
+ if (beginCount === 0 && endCount === 0)
42
+ return { status: 'unmanaged', path, exists: true, action: 'append-managed-block', backupRequired: true, message: 'Claude user memory exists without a VGXNESS managed block; apply would append one after backup.', contents };
43
+ if (beginCount === 0)
44
+ return blocked(path, true, 'missing_begin_marker', 'Claude user memory has an end marker without a begin marker.');
45
+ if (endCount === 0)
46
+ return blocked(path, true, 'missing_end_marker', 'Claude user memory has a begin marker without an end marker.');
47
+ if (beginCount > 1 || endCount > 1)
48
+ return blocked(path, true, 'duplicate_markers', 'Claude user memory has duplicate VGXNESS managed-block markers.');
49
+ const start = contents.indexOf(claudeUserMemoryBeginMarker);
50
+ const endMarkerStart = contents.indexOf(claudeUserMemoryEndMarker);
51
+ if (endMarkerStart < start)
52
+ return blocked(path, true, 'reordered_markers', 'Claude user memory end marker appears before begin marker.');
53
+ const end = endMarkerStart + claudeUserMemoryEndMarker.length;
54
+ const inner = contents.slice(start + claudeUserMemoryBeginMarker.length, endMarkerStart);
55
+ if (inner.includes(claudeUserMemoryBeginMarker) || inner.includes(claudeUserMemoryEndMarker))
56
+ return blocked(path, true, 'nested_markers', 'Claude user memory has nested VGXNESS managed-block markers.');
57
+ if (!inner.includes('owner=vgxness') || !inner.includes('provider=claude') || !inner.includes('artifact=claude-user-memory') || !inner.includes('safe-update=true'))
58
+ return blocked(path, true, 'conflicting_ownership', 'Claude user memory managed block is not a VGXNESS-owned user-memory block.');
59
+ const expected = renderClaudeUserMemoryBlock();
60
+ const actual = contents.slice(start, end);
61
+ if (actual === expected)
62
+ return { status: 'managed-current', path, exists: true, action: 'none', backupRequired: false, message: 'Claude user memory has the current VGXNESS managed block.', blockRange: { start, end }, contents };
63
+ return { status: 'managed-stale', path, exists: true, action: 'update-managed-block', backupRequired: true, message: 'Claude user memory has a stale VGXNESS managed block; apply would update only that block after backup.', blockRange: { start, end }, staleReasons: ['managed_block_differs_from_canonical_render'], contents };
64
+ }
65
+ export function mergeClaudeUserMemory(state) {
66
+ const block = renderClaudeUserMemoryBlock();
67
+ if (state.status === 'missing')
68
+ return { ok: true, value: { path: state.path, contents: `${block}\n` } };
69
+ if (state.status === 'unmanaged') {
70
+ const separator = state.contents.length === 0 ? '' : state.contents.endsWith('\n') ? '\n' : '\n\n';
71
+ return { ok: true, value: { path: state.path, contents: `${state.contents}${separator}${block}\n` } };
72
+ }
73
+ if (state.status === 'managed-stale')
74
+ return { ok: true, value: { path: state.path, contents: state.contents.slice(0, state.blockRange.start) + block + state.contents.slice(state.blockRange.end) } };
75
+ if (state.status === 'managed-current')
76
+ return { ok: true, value: { path: state.path, contents: state.contents } };
77
+ return { ok: false, error: { code: 'validation_failed', message: state.message } };
78
+ }
79
+ function blocked(path, exists, reason, message) {
80
+ return { status: 'blocked', path, exists, action: 'blocked', backupRequired: false, reason, message };
81
+ }
82
+ function countOccurrences(contents, needle) {
83
+ let count = 0;
84
+ let index = contents.indexOf(needle);
85
+ while (index !== -1) {
86
+ count += 1;
87
+ index = contents.indexOf(needle, index + needle.length);
88
+ }
89
+ return count;
90
+ }
@@ -1,9 +1,27 @@
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';
6
+ import { inspectClaudeCodeUserMcpConfig, resolveClaudeCodeUserMcpJsonPath } from './claude-code-user-config.js';
7
+ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
3
8
  export function planClaudeCodeMcpInstall(input) {
4
9
  const source = input.databasePathSource ?? 'flag';
5
10
  const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
6
11
  const overwriteVgxness = input.overwriteVgxness === true;
12
+ const resolvedScope = resolveClaudeCodeScope(input.scope);
13
+ if (!resolvedScope.ok)
14
+ return refused(input, server, 'unsupported_scope', resolvedScope.error.message, [], [], overwriteVgxness);
15
+ const cliCommand = buildClaudeCodeMcpAddCommand({ scope: resolvedScope.value.canonical });
16
+ if (!cliCommand.ok)
17
+ return refused(input, server, 'unsupported_scope', cliCommand.error.message, [], [], overwriteVgxness);
18
+ if (resolvedScope.value.canonical !== 'project') {
19
+ if (resolvedScope.value.canonical === 'local') {
20
+ const targets = [{ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' }];
21
+ return { ...base(input, server, targets, [], false, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings), status: 'would_install' };
22
+ }
23
+ return planUserInstall(input, server, overwriteVgxness, cliCommand.value, resolvedScope.value.warnings);
24
+ }
7
25
  let mcpPath;
8
26
  try {
9
27
  mcpPath = resolveClaudeCodeMcpJsonPath(input.cwd);
@@ -13,6 +31,7 @@ export function planClaudeCodeMcpInstall(input) {
13
31
  }
14
32
  const mcpState = inspectClaudeCodeMcpConfig(input.cwd);
15
33
  const targets = [];
34
+ targets.push({ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' });
16
35
  const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
17
36
  if (mcpState.status === 'missing')
18
37
  targets.push({ kind: 'mcp-json', path: mcpPath, action: 'create' });
@@ -25,46 +44,87 @@ export function planClaudeCodeMcpInstall(input) {
25
44
  const agentInspection = inspectClaudeCodeAgents(input.cwd);
26
45
  for (const agent of agentInspection.agents) {
27
46
  if (agent.status === 'missing')
28
- targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'create' });
47
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'create' });
29
48
  else if (agent.status === 'managed')
30
- targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'update-vgxness' });
49
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'update-vgxness' });
31
50
  else
32
- targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'blocked', reason: agent.detail });
51
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'blocked', reason: agent.detail });
33
52
  }
53
+ const projectMemory = inspectClaudeProjectMemory(input.cwd);
54
+ targets.push({ kind: 'project-memory', path: projectMemory.path, action: projectMemory.action, status: projectMemory.status, backupRequired: projectMemory.backupRequired, ...(projectMemory.status === 'blocked' ? { reason: projectMemory.message } : {}) });
34
55
  const blocked = targets.find((target) => target.action === 'blocked');
35
56
  if (blocked !== undefined) {
36
- const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : 'existing_vgxness_agent';
57
+ const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : blocked.kind === 'project-memory' ? projectMemoryRefusalReason(projectMemory) : 'existing_vgxness_agent';
37
58
  return refused(input, server, reason, blocked.reason ?? 'Claude Code install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
38
59
  }
39
- const backupRequired = targets.some((target) => (target.kind === 'mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness'));
60
+ 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
61
  return {
41
- ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness),
62
+ ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings),
42
63
  status: 'would_install',
43
64
  };
44
65
  }
45
66
  export function expectedClaudeCodeRenderedAgents(workspaceRoot) {
46
67
  return expectedClaudeCodeAgentFiles(workspaceRoot).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
47
68
  }
69
+ export function expectedClaudeCodeRenderedUserAgents(workspaceRoot, env) {
70
+ return expectedClaudeCodeAgentFiles({ workspaceRoot, scope: 'user', ...(env === undefined ? {} : { env }) }).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
71
+ }
72
+ function planUserInstall(input, server, overwriteVgxness, cliCommand, scopeWarnings) {
73
+ const mcpState = inspectClaudeCodeUserMcpConfig(input.env);
74
+ const targets = [{ kind: 'cli-mcp-registration', scope: 'user', command: cliCommand, action: 'register' }];
75
+ const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
76
+ if (mcpState.status === 'missing')
77
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'create' });
78
+ else if (mcpState.status === 'stale')
79
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'merge' });
80
+ else if (mcpState.status === 'configured')
81
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'update-vgxness' });
82
+ else
83
+ targets.push({ kind: 'user-mcp-json', path: mcpState.path, external: true, action: 'blocked', reason: mcpState.message });
84
+ const agentInspection = inspectClaudeCodeAgents({ workspaceRoot: input.cwd, scope: 'user', ...(input.env === undefined ? {} : { env: input.env }) });
85
+ for (const agent of agentInspection.agents) {
86
+ if (agent.status === 'missing')
87
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'create' });
88
+ else if (agent.status === 'managed')
89
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'update-vgxness' });
90
+ else
91
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'user', external: true, action: 'blocked', reason: agent.detail });
92
+ }
93
+ const userMemory = inspectClaudeUserMemory(input.env);
94
+ targets.push({ kind: 'user-memory', path: userMemory.path, external: true, action: userMemory.action, status: userMemory.status, backupRequired: userMemory.backupRequired, ...(userMemory.status === 'blocked' ? { reason: userMemory.message } : {}) });
95
+ const blocked = targets.find((target) => target.action === 'blocked');
96
+ if (blocked !== undefined) {
97
+ const reason = blocked.kind === 'user-mcp-json' ? userMcpRefusalReason(mcpState.status) : blocked.kind === 'user-memory' ? userMemoryRefusalReason(userMemory) : 'existing_vgxness_agent';
98
+ return refused(input, server, reason, blocked.reason ?? 'Claude user install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
99
+ }
100
+ const backupRequired = targets.some((target) => (target.kind === 'user-mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness') || (target.kind === 'user-memory' && target.backupRequired));
101
+ return { ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, 'user', cliCommand, scopeWarnings), status: 'would_install' };
102
+ }
48
103
  function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
49
- return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness), status: 'refused', reason, message };
104
+ const resolved = resolveClaudeCodeScope(input.scope);
105
+ const canonical = resolved.ok ? resolved.value.canonical : 'project';
106
+ const cli = buildClaudeCodeMcpAddCommand({ scope: canonical });
107
+ return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness, canonical, cli.ok ? cli.value : undefined, resolved.ok ? resolved.value.warnings : []), status: 'refused', reason, message };
50
108
  }
51
- function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness) {
109
+ function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, canonicalClaudeScope, cliCommand, scopeWarnings) {
52
110
  const source = input.databasePathSource ?? 'flag';
53
- const targetPath = resolveClaudeCodeMcpJsonPath(input.cwd);
111
+ const targetPath = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : canonicalClaudeScope === 'user' ? resolveClaudeCodeUserMcpJsonPath(input.env) : `claude-cli:${canonicalClaudeScope}:vgxness`;
54
112
  return {
55
113
  version: 1,
56
114
  kind: 'mcp-client-install-claude-code',
57
115
  installable: true,
58
116
  mutating: false,
59
117
  provider: 'claude',
60
- scope: 'project',
118
+ scope: input.scope ?? 'project',
61
119
  targetPath,
62
120
  targets,
63
121
  backupRequired,
64
122
  safety: { operation: 'plan', mutating: false, writesProviderConfig: false, targetPath, backupRequired, mergePolicy: backupRequired ? 'merge-preserve-existing' : 'create' },
65
123
  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.',
124
+ ...scopeWarnings,
125
+ 'Claude Code MCP registration is modeled as structured config/argv, never shell strings.',
126
+ 'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight.',
127
+ 'Claude user/global support narrowly merges only mcpServers.vgxness in ~/.claude.json and writes VGXNESS-owned ~/.claude/agents/*.md plus a managed block in ~/.claude/CLAUDE.md after confirmation/preflight; unknown config keys and non-managed memory content are preserved.',
68
128
  ],
69
129
  verificationHints: [
70
130
  { kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed project config installation.' },
@@ -72,6 +132,8 @@ function base(input, server, targets, preservedTopLevelKeys, backupRequired, ove
72
132
  { kind: 'command', message: 'Run the MCP doctor command after installation.', command: createClaudeCodeMcpDoctorCommand(input.databasePath, source) },
73
133
  ],
74
134
  server,
135
+ ...(cliCommand === undefined ? {} : { cliCommand }),
136
+ canonicalClaudeScope,
75
137
  preservedTopLevelKeys,
76
138
  agentNames: targets.filter((target) => target.kind === 'agent-file').map((target) => target.agentName),
77
139
  overwriteVgxness,
@@ -84,3 +146,20 @@ function mcpRefusalReason(status) {
84
146
  return 'existing_vgxness_mcp';
85
147
  return 'invalid_mcp_shape';
86
148
  }
149
+ function userMcpRefusalReason(status) {
150
+ if (status === 'invalid')
151
+ return 'malformed_json';
152
+ if (status === 'conflicting')
153
+ return 'existing_vgxness_mcp';
154
+ return 'invalid_mcp_shape';
155
+ }
156
+ function projectMemoryRefusalReason(state) {
157
+ if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
158
+ return 'conflicting_claude_project_memory';
159
+ return 'malformed_claude_project_memory';
160
+ }
161
+ function userMemoryRefusalReason(state) {
162
+ if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
163
+ return 'conflicting_claude_project_memory';
164
+ return 'malformed_claude_project_memory';
165
+ }