vgxness 1.5.1 → 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 (56) hide show
  1. package/README.md +23 -2
  2. package/dist/agents/agent-seed-service.js +10 -0
  3. package/dist/agents/canonical-agent-manifest.js +177 -0
  4. package/dist/agents/canonical-agent-projection.js +164 -0
  5. package/dist/agents/renderers/claude-renderer.js +30 -52
  6. package/dist/cli/bun-bin.js +6 -0
  7. package/dist/cli/cli-flags.js +1 -1
  8. package/dist/cli/cli-help.js +7 -4
  9. package/dist/cli/commands/agent-skill-dispatcher.js +6 -5
  10. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +2 -2
  11. package/dist/cli/commands/mcp-dispatcher.js +75 -3
  12. package/dist/cli/commands/setup-dispatcher.js +9 -0
  13. package/dist/cli/index.js +1 -1
  14. package/dist/cli/tui/main-menu/main-menu-read-model.js +41 -44
  15. package/dist/cli/tui/main-menu/main-menu-render-shape.js +15 -15
  16. package/dist/cli/tui/opentui/main-menu/screen.js +39 -41
  17. package/dist/cli/tui/opentui/main-menu/smoke.js +1 -1
  18. package/dist/cli/tui/opentui/main-menu/view.js +1 -1
  19. package/dist/cli/tui/setup/setup-tui-read-model.js +15 -12
  20. package/dist/governance/governance-report-builder.js +45 -26
  21. package/dist/mcp/claude-code-agent-config.js +95 -0
  22. package/dist/mcp/claude-code-cli.js +71 -0
  23. package/dist/mcp/claude-code-config.js +84 -0
  24. package/dist/mcp/claude-code-project-memory.js +127 -0
  25. package/dist/mcp/claude-code-scope.js +18 -0
  26. package/dist/mcp/client-install-claude-code-contract.js +114 -0
  27. package/dist/mcp/client-install-claude-code.js +136 -0
  28. package/dist/mcp/index.js +8 -0
  29. package/dist/mcp/opencode-default-agent-config.js +7 -113
  30. package/dist/mcp/provider-canonical-agent-manifest.js +39 -0
  31. package/dist/mcp/provider-change-plan.js +109 -1
  32. package/dist/mcp/provider-doctor.js +105 -1
  33. package/dist/mcp/provider-health-types.js +4 -0
  34. package/dist/mcp/provider-status.js +159 -3
  35. package/dist/mcp/schema.js +6 -5
  36. package/dist/mcp/validation.js +1 -1
  37. package/dist/memory/memory-service.js +4 -0
  38. package/dist/sdd/sdd-workflow-service.js +129 -59
  39. package/dist/setup/providers/claude-setup-adapter.js +13 -8
  40. package/dist/setup/setup-plan.js +60 -1
  41. package/docs/architecture.md +55 -113
  42. package/docs/cli.md +90 -2
  43. package/docs/code-runtime.md +218 -0
  44. package/docs/contributing.md +120 -0
  45. package/docs/glossary.md +211 -0
  46. package/docs/mcp.md +144 -0
  47. package/docs/prd.md +23 -26
  48. package/docs/providers.md +150 -0
  49. package/docs/roadmap.md +88 -0
  50. package/docs/safety.md +147 -0
  51. package/docs/storage.md +93 -0
  52. package/package.json +1 -1
  53. package/docs/funcionamiento-del-sistema.md +0 -865
  54. package/docs/harness-gap-analysis.md +0 -243
  55. package/docs/vgxcode.md +0 -87
  56. package/docs/vgxness-code.md +0 -48
@@ -59,18 +59,53 @@ export class GovernanceReportBuilder {
59
59
  warnings.push('missing-change-sdd-snapshot-skipped');
60
60
  return { ok: true, value: report };
61
61
  }
62
- const status = this.services.sdd.getStatus({
63
- project: input.project,
62
+ if (this.tryBuildOptimizedSddSnapshot(report, input.project, change, phase, payloadMode, warnings)) {
63
+ report.warnings = dedupe(warnings);
64
+ report.openCode.warnings = dedupe(report.openCode.warnings ?? []);
65
+ return { ok: true, value: report };
66
+ }
67
+ this.buildFallbackSddSnapshot(report, input.project, change, phase, payloadMode, warnings);
68
+ report.warnings = dedupe(warnings);
69
+ report.openCode.warnings = dedupe(report.openCode.warnings ?? []);
70
+ return { ok: true, value: report };
71
+ }
72
+ tryBuildOptimizedSddSnapshot(report, project, change, phase, payloadMode, warnings) {
73
+ if (this.services.sdd.getGovernanceSnapshot === undefined)
74
+ return false;
75
+ if (phase !== undefined && !isSddPhase(phase)) {
76
+ warnings.push('sdd-readiness-skipped-non-sdd-phase');
77
+ return false;
78
+ }
79
+ const snapshot = this.services.sdd.getGovernanceSnapshot({
80
+ project,
64
81
  change,
82
+ payloadMode,
83
+ ...(phase === undefined ? {} : { phase }),
65
84
  });
85
+ if (!snapshot.ok) {
86
+ warnings.push(`sdd-snapshot-unavailable:${snapshot.error.code}`);
87
+ return false;
88
+ }
89
+ report.sdd.status = snapshot.value.status;
90
+ report.sdd.artifacts = snapshot.value.artifacts;
91
+ warnings.push(...snapshot.value.warnings);
92
+ if (snapshot.value.readiness !== undefined) {
93
+ report.sdd.readiness = snapshot.value.readiness;
94
+ for (const blocker of snapshot.value.readiness.blockedPrerequisites ?? [])
95
+ warnings.push(`missing-accepted-prerequisite:${blocker.phase}:${blocker.reason}`);
96
+ }
97
+ else {
98
+ warnings.push('sdd-readiness-skipped-missing-phase');
99
+ }
100
+ return true;
101
+ }
102
+ buildFallbackSddSnapshot(report, project, change, phase, payloadMode, warnings) {
103
+ const status = this.services.sdd.getStatus({ project, change });
66
104
  if (status.ok)
67
105
  report.sdd.status = status.value;
68
106
  else
69
107
  warnings.push(`sdd-status-unavailable:${status.error.code}`);
70
- const listed = this.services.sdd.listArtifacts({
71
- project: input.project,
72
- change,
73
- });
108
+ const listed = this.services.sdd.listArtifacts({ project, change });
74
109
  if (listed.ok) {
75
110
  report.sdd.artifacts = listed.value.artifacts.map((artifact) => {
76
111
  const envelope = normalizeSddArtifact(artifact);
@@ -81,34 +116,21 @@ export class GovernanceReportBuilder {
81
116
  phase: artifact.phase,
82
117
  topicKey: artifact.topicKey,
83
118
  artifact: compactArtifact,
84
- envelope: {
85
- ...envelope,
86
- artifact: compactArtifact,
87
- },
119
+ envelope: { ...envelope, artifact: compactArtifact },
88
120
  };
89
121
  }
90
- return {
91
- phase: artifact.phase,
92
- topicKey: artifact.topicKey,
93
- artifact,
94
- envelope,
95
- };
122
+ return { phase: artifact.phase, topicKey: artifact.topicKey, artifact, envelope };
96
123
  });
97
124
  }
98
125
  else {
99
126
  warnings.push(`sdd-artifacts-unavailable:${listed.error.code}`);
100
127
  }
101
128
  if (phase !== undefined && isSddPhase(phase)) {
102
- const readiness = this.services.sdd.getReady({
103
- project: input.project,
104
- change,
105
- phase,
106
- });
129
+ const readiness = this.services.sdd.getReady({ project, change, phase });
107
130
  if (readiness.ok) {
108
131
  report.sdd.readiness = readiness.value;
109
- for (const blocker of readiness.value.blockedPrerequisites ?? []) {
132
+ for (const blocker of readiness.value.blockedPrerequisites ?? [])
110
133
  warnings.push(`missing-accepted-prerequisite:${blocker.phase}:${blocker.reason}`);
111
- }
112
134
  }
113
135
  else {
114
136
  warnings.push(`sdd-readiness-unavailable:${readiness.error.code}`);
@@ -120,9 +142,6 @@ export class GovernanceReportBuilder {
120
142
  else {
121
143
  warnings.push('sdd-readiness-skipped-missing-phase');
122
144
  }
123
- report.warnings = dedupe(warnings);
124
- report.openCode.warnings = dedupe(report.openCode.warnings ?? []);
125
- return { ok: true, value: report };
126
145
  }
127
146
  buildOverlay(input, run, agent, warnings) {
128
147
  if (this.services.managerProfiles === undefined)
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { projectCanonicalAgentManifestToClaudeCode } from '../agents/canonical-agent-projection.js';
5
+ import { assertInsideWorkspace } from './claude-code-config.js';
6
+ export const claudeCodeGeneratedMarker = 'VGXNESS-GENERATED';
7
+ export function expectedClaudeCodeAgentFiles(input) {
8
+ const target = typeof input === 'string' ? resolveClaudeAgentTarget({ workspaceRoot: input, scope: 'project' }) : resolveClaudeAgentTarget(input);
9
+ return projectCanonicalAgentManifestToClaudeCode().agents.map((agent) => {
10
+ const path = resolve(target.directoryPath, agent.fileName);
11
+ if (target.scope === 'project')
12
+ assertInsideWorkspace(target.workspaceRoot, path);
13
+ return { ...agent, path };
14
+ });
15
+ }
16
+ export function renderClaudeCodeAgentMarkdown(agent) {
17
+ return `---\nname: ${yamlScalar(agent.name)}\ndescription: ${yamlScalar(agent.description)}\n---\n\n<!-- ${claudeCodeGeneratedMarker} claude-code-provider-support provider=claude artifact=claude-code-subagent promptContractVersion=${agent.promptContractVersion} safe-update=true -->\n\n${agent.instructions.trim()}\n`;
18
+ }
19
+ export function inspectClaudeCodeAgents(input) {
20
+ const target = typeof input === 'string' ? resolveClaudeAgentTarget({ workspaceRoot: input, scope: 'project' }) : resolveClaudeAgentTarget(input);
21
+ const expected = expectedClaudeCodeAgentFiles({ workspaceRoot: target.workspaceRoot, scope: target.scope, ...(target.env === undefined ? {} : { env: target.env }) });
22
+ return { scope: target.scope, directoryPath: target.directoryPath, directoryExists: existsSync(target.directoryPath), external: target.scope === 'user', agents: expected.map(inspectAgent) };
23
+ }
24
+ export function resolveClaudeAgentTarget(input) {
25
+ const scope = input.scope ?? 'project';
26
+ if (scope === 'project')
27
+ return { workspaceRoot: input.workspaceRoot, scope, directoryPath: join(input.workspaceRoot, '.claude', 'agents'), ...(input.env === undefined ? {} : { env: input.env }) };
28
+ const home = safeHomeDirectory(input.env);
29
+ return { workspaceRoot: input.workspaceRoot, scope, directoryPath: join(home, '.claude', 'agents'), ...(input.env === undefined ? {} : { env: input.env }) };
30
+ }
31
+ export function safeHomeDirectory(env = process.env) {
32
+ const candidate = env.HOME?.trim() || homedir();
33
+ if (!candidate || candidate === '/' || candidate.includes('\0') || candidate.includes('~'))
34
+ throw new Error('Unable to resolve a safe home directory for Claude user agents.');
35
+ return resolve(candidate);
36
+ }
37
+ export function isVgxnessOwnedClaudeAgentMarkdown(contents) {
38
+ return contents.includes(claudeCodeGeneratedMarker) && contents.includes('provider=claude') && contents.includes('artifact=claude-code-subagent');
39
+ }
40
+ export function parseClaudeAgentFrontmatter(contents) {
41
+ if (!contents.startsWith('---\n'))
42
+ return { ok: false, reason: 'Missing YAML frontmatter.' };
43
+ const end = contents.indexOf('\n---\n', 4);
44
+ if (end < 0)
45
+ return { ok: false, reason: 'Unclosed YAML frontmatter.' };
46
+ const lines = contents.slice(4, end).split('\n');
47
+ const data = {};
48
+ for (const line of lines) {
49
+ const index = line.indexOf(':');
50
+ if (index <= 0)
51
+ return { ok: false, reason: `Invalid frontmatter line: ${line}` };
52
+ const key = line.slice(0, index).trim();
53
+ const value = line.slice(index + 1).trim();
54
+ data[key] = unquoteYamlScalar(value);
55
+ }
56
+ if (!data.name?.trim())
57
+ return { ok: false, reason: 'Frontmatter name is required.' };
58
+ if (!data.description?.trim())
59
+ return { ok: false, reason: 'Frontmatter description is required.' };
60
+ return { ok: true, data: { name: data.name, description: data.description } };
61
+ }
62
+ function inspectAgent(agent) {
63
+ if (!existsSync(agent.path))
64
+ return { agentName: agent.name, path: agent.path, exists: false, status: 'missing', frontmatter: 'missing', generatedMarker: false, detail: 'Expected Claude agent file is missing.' };
65
+ try {
66
+ const contents = readFileSync(agent.path, 'utf8');
67
+ const frontmatter = parseClaudeAgentFrontmatter(contents);
68
+ const generatedMarker = isVgxnessOwnedClaudeAgentMarkdown(contents);
69
+ if (!frontmatter.ok)
70
+ return { agentName: agent.name, path: agent.path, exists: true, status: 'invalid', frontmatter: 'invalid', generatedMarker, detail: frontmatter.reason };
71
+ if (frontmatter.data.name !== agent.name)
72
+ return { agentName: agent.name, path: agent.path, exists: true, status: 'conflicting', frontmatter: 'valid', generatedMarker, detail: `Frontmatter name ${frontmatter.data.name} does not match expected ${agent.name}.` };
73
+ if (!generatedMarker)
74
+ return { agentName: agent.name, path: agent.path, exists: true, status: 'conflicting', frontmatter: 'valid', generatedMarker, detail: 'Existing agent file is not marked as VGXNESS-generated.' };
75
+ return { agentName: agent.name, path: agent.path, exists: true, status: 'managed', frontmatter: 'valid', generatedMarker, detail: 'Claude agent file is VGXNESS-managed.' };
76
+ }
77
+ catch (cause) {
78
+ const message = cause instanceof Error ? cause.message : String(cause);
79
+ return { agentName: agent.name, path: agent.path, exists: true, status: 'invalid', frontmatter: 'invalid', generatedMarker: false, detail: `Unable to read Claude agent file: ${message}` };
80
+ }
81
+ }
82
+ function yamlScalar(value) {
83
+ return JSON.stringify(value);
84
+ }
85
+ function unquoteYamlScalar(value) {
86
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
87
+ try {
88
+ return value.startsWith('"') ? JSON.parse(value) : value.slice(1, -1).replaceAll("''", "'");
89
+ }
90
+ catch {
91
+ return value.slice(1, -1);
92
+ }
93
+ }
94
+ return value;
95
+ }
@@ -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
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, relative, resolve } from 'node:path';
3
+ export function resolveClaudeCodeMcpJsonPath(workspaceRoot) {
4
+ const target = resolve(workspaceRoot, '.mcp.json');
5
+ assertInsideWorkspace(workspaceRoot, target);
6
+ return target;
7
+ }
8
+ export function createClaudeCodeMcpServerConfig(databasePath, source = 'flag') {
9
+ return { type: 'stdio', command: 'vgxness', args: source === 'global-default' ? ['mcp', 'start'] : ['mcp', 'start', '--db', databasePath] };
10
+ }
11
+ export function createClaudeCodeMcpDoctorCommand(databasePath, source) {
12
+ return source === 'global-default' ? ['vgxness', 'mcp', 'doctor'] : ['vgxness', 'mcp', 'doctor', '--db', databasePath];
13
+ }
14
+ export function isManagedClaudeCodeMcpServer(value) {
15
+ return isRecord(value) && value.type === 'stdio' && value.command === 'vgxness' && isManagedClaudeArgs(value.args);
16
+ }
17
+ export function isManagedClaudeArgs(value) {
18
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string'))
19
+ return false;
20
+ const dbPath = value[3];
21
+ return arraysEqual(value, ['mcp', 'start']) || (value.length === 4 && value[0] === 'mcp' && value[1] === 'start' && value[2] === '--db' && typeof dbPath === 'string' && dbPath.trim().length > 0);
22
+ }
23
+ export function inspectClaudeCodeMcpConfig(workspaceRoot) {
24
+ const path = resolveClaudeCodeMcpJsonPath(workspaceRoot);
25
+ if (!existsSync(path))
26
+ return { status: 'missing', path, exists: false, parsed: false, message: 'Claude project .mcp.json does not exist.' };
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
29
+ if (!isRecord(parsed))
30
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude project .mcp.json must be a JSON object.' };
31
+ if (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers)) {
32
+ return { status: 'invalid', path, exists: true, parsed: false, message: 'Claude project .mcp.json mcpServers must be a JSON object.' };
33
+ }
34
+ const entry = isRecord(parsed.mcpServers) ? parsed.mcpServers.vgxness : undefined;
35
+ if (entry === undefined)
36
+ return { status: 'stale', path, exists: true, parsed: true, config: parsed, message: 'Claude project .mcp.json is readable but mcpServers.vgxness is missing.' };
37
+ if (isManagedClaudeCodeMcpServer(entry))
38
+ return { status: 'configured', path, exists: true, parsed: true, config: parsed, message: 'Claude project .mcp.json has a managed mcpServers.vgxness entry.' };
39
+ return { status: 'conflicting', path, exists: true, parsed: true, config: parsed, message: 'Claude project .mcp.json has a conflicting mcpServers.vgxness entry.' };
40
+ }
41
+ catch (cause) {
42
+ const message = cause instanceof Error ? cause.message : String(cause);
43
+ return { status: 'invalid', path, exists: true, parsed: false, message: `Claude project .mcp.json could not be read or parsed: ${message}` };
44
+ }
45
+ }
46
+ export function mergeClaudeCodeMcpConfig(existing, server) {
47
+ return { ...existing, mcpServers: { ...(isRecord(existing.mcpServers) ? existing.mcpServers : {}), vgxness: server } };
48
+ }
49
+ export function claudeMcpConfigPathStatus(state) {
50
+ return {
51
+ label: 'project .mcp.json',
52
+ path: state.path,
53
+ exists: state.exists,
54
+ readable: state.status !== 'invalid',
55
+ parsed: state.parsed,
56
+ status: state.status === 'configured' ? 'pass' : state.status === 'invalid' || state.status === 'conflicting' ? 'fail' : 'not-configured',
57
+ detail: state.message,
58
+ };
59
+ }
60
+ export function claudeMcpEntryStatus(state) {
61
+ if (state.status === 'configured')
62
+ return { configured: true, status: 'pass', serverName: 'vgxness', enabled: true, detail: state.message };
63
+ if (state.status === 'conflicting')
64
+ return { configured: true, status: 'fail', serverName: 'vgxness', detail: state.message };
65
+ if (state.status === 'invalid')
66
+ return { configured: false, status: 'fail', serverName: 'vgxness', detail: state.message };
67
+ return { configured: false, status: 'not-configured', serverName: 'vgxness', detail: state.message };
68
+ }
69
+ export function assertInsideWorkspace(workspaceRoot, targetPath) {
70
+ const root = resolve(workspaceRoot);
71
+ const target = resolve(targetPath);
72
+ const rel = relative(root, target);
73
+ if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || rel === '' && target !== root)
74
+ throw new Error(`Target path is outside workspace root: ${targetPath}`);
75
+ }
76
+ export function claudeAdvisoryPaths(workspaceRoot) {
77
+ return [join(workspaceRoot, '.claude', 'settings.json'), join(workspaceRoot, '.claude', 'settings.local.json'), join(workspaceRoot, '.claude', 'CLAUDE.md')];
78
+ }
79
+ function arraysEqual(left, right) {
80
+ return left.length === right.length && left.every((value, index) => value === right[index]);
81
+ }
82
+ function isRecord(value) {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
84
+ }
@@ -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,114 @@
1
+ import { expectedClaudeCodeAgentFiles, inspectClaudeCodeAgents, renderClaudeCodeAgentMarkdown } from './claude-code-agent-config.js';
2
+ import { buildClaudeCodeMcpAddCommand } from './claude-code-cli.js';
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
+ export function planClaudeCodeMcpInstall(input) {
7
+ const source = input.databasePathSource ?? 'flag';
8
+ const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
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
+ }
20
+ let mcpPath;
21
+ try {
22
+ mcpPath = resolveClaudeCodeMcpJsonPath(input.cwd);
23
+ }
24
+ catch (cause) {
25
+ return refused(input, server, 'outside_workspace', cause instanceof Error ? cause.message : String(cause), [], [], overwriteVgxness);
26
+ }
27
+ const mcpState = inspectClaudeCodeMcpConfig(input.cwd);
28
+ const targets = [];
29
+ targets.push({ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' });
30
+ const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
31
+ if (mcpState.status === 'missing')
32
+ targets.push({ kind: 'mcp-json', path: mcpPath, action: 'create' });
33
+ else if (mcpState.status === 'stale')
34
+ targets.push({ kind: 'mcp-json', path: mcpPath, action: 'merge' });
35
+ else if (mcpState.status === 'configured')
36
+ targets.push({ kind: 'mcp-json', path: mcpPath, action: 'update-vgxness' });
37
+ else
38
+ targets.push({ kind: 'mcp-json', path: mcpPath, action: 'blocked', reason: mcpState.message });
39
+ const agentInspection = inspectClaudeCodeAgents(input.cwd);
40
+ for (const agent of agentInspection.agents) {
41
+ if (agent.status === 'missing')
42
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'create' });
43
+ else if (agent.status === 'managed')
44
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'update-vgxness' });
45
+ else
46
+ targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'blocked', reason: agent.detail });
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 } : {}) });
50
+ const blocked = targets.find((target) => target.action === 'blocked');
51
+ if (blocked !== undefined) {
52
+ const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : blocked.kind === 'project-memory' ? projectMemoryRefusalReason(projectMemory) : 'existing_vgxness_agent';
53
+ return refused(input, server, reason, blocked.reason ?? 'Claude Code install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
54
+ }
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));
56
+ return {
57
+ ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings),
58
+ status: 'would_install',
59
+ };
60
+ }
61
+ export function expectedClaudeCodeRenderedAgents(workspaceRoot) {
62
+ return expectedClaudeCodeAgentFiles(workspaceRoot).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
63
+ }
64
+ function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
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 };
69
+ }
70
+ function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, canonicalClaudeScope, cliCommand, scopeWarnings) {
71
+ const source = input.databasePathSource ?? 'flag';
72
+ const targetPath = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : `claude-cli:${canonicalClaudeScope}:vgxness`;
73
+ return {
74
+ version: 1,
75
+ kind: 'mcp-client-install-claude-code',
76
+ installable: true,
77
+ mutating: false,
78
+ provider: 'claude',
79
+ scope: input.scope ?? 'project',
80
+ targetPath,
81
+ targets,
82
+ backupRequired,
83
+ safety: { operation: 'plan', mutating: false, writesProviderConfig: false, targetPath, backupRequired, mergePolicy: backupRequired ? 'merge-preserve-existing' : 'create' },
84
+ warnings: [
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.',
89
+ ],
90
+ verificationHints: [
91
+ { kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed project config installation.' },
92
+ { kind: 'manual-check', message: 'Open the project in Claude Code and verify the vgxness MCP server and project agents are visible.' },
93
+ { kind: 'command', message: 'Run the MCP doctor command after installation.', command: createClaudeCodeMcpDoctorCommand(input.databasePath, source) },
94
+ ],
95
+ server,
96
+ ...(cliCommand === undefined ? {} : { cliCommand }),
97
+ canonicalClaudeScope,
98
+ preservedTopLevelKeys,
99
+ agentNames: targets.filter((target) => target.kind === 'agent-file').map((target) => target.agentName),
100
+ overwriteVgxness,
101
+ };
102
+ }
103
+ function mcpRefusalReason(status) {
104
+ if (status === 'invalid')
105
+ return 'malformed_json';
106
+ if (status === 'conflicting')
107
+ return 'existing_vgxness_mcp';
108
+ return 'invalid_mcp_shape';
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
+ }