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.
- 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/claude-code-user-config.js +55 -0
- package/dist/mcp/claude-code-user-memory.js +90 -0
- package/dist/mcp/client-install-claude-code-contract.js +91 -12
- package/dist/mcp/client-install-claude-code.js +133 -12
- package/dist/mcp/control-plane.js +18 -1
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/provider-change-plan.js +18 -6
- package/dist/mcp/provider-doctor.js +71 -5
- package/dist/mcp/provider-health-types.js +4 -0
- package/dist/mcp/provider-status.js +77 -8
- package/dist/mcp/schema.js +4 -3
- package/dist/sdd/schema.js +15 -0
- package/dist/sdd/sdd-workflow-service.js +59 -29
- package/dist/setup/providers/claude-setup-adapter.js +11 -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
67
|
-
'
|
|
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
|
+
}
|