vgxness 1.5.1 → 1.5.2
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/README.md +23 -2
- package/dist/agents/agent-seed-service.js +10 -0
- package/dist/agents/canonical-agent-manifest.js +177 -0
- package/dist/agents/canonical-agent-projection.js +146 -0
- package/dist/agents/renderers/claude-renderer.js +30 -52
- package/dist/cli/bun-bin.js +6 -0
- package/dist/cli/cli-help.js +3 -0
- package/dist/cli/commands/agent-skill-dispatcher.js +6 -5
- package/dist/cli/commands/mcp-dispatcher.js +65 -3
- package/dist/cli/index.js +1 -1
- package/dist/governance/governance-report-builder.js +45 -26
- package/dist/mcp/claude-code-agent-config.js +79 -0
- package/dist/mcp/claude-code-config.js +84 -0
- package/dist/mcp/client-install-claude-code-contract.js +86 -0
- package/dist/mcp/client-install-claude-code.js +85 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/opencode-default-agent-config.js +7 -113
- package/dist/mcp/provider-canonical-agent-manifest.js +39 -0
- package/dist/mcp/provider-change-plan.js +57 -1
- package/dist/mcp/provider-doctor.js +54 -0
- package/dist/mcp/provider-status.js +82 -2
- package/dist/mcp/schema.js +2 -2
- package/dist/mcp/validation.js +1 -1
- package/dist/memory/memory-service.js +4 -0
- package/dist/sdd/sdd-workflow-service.js +129 -59
- package/dist/setup/providers/claude-setup-adapter.js +7 -4
- package/docs/architecture.md +54 -112
- package/docs/cli.md +53 -0
- package/docs/code-runtime.md +218 -0
- package/docs/contributing.md +120 -0
- package/docs/glossary.md +211 -0
- package/docs/mcp.md +144 -0
- package/docs/prd.md +23 -26
- package/docs/providers.md +123 -0
- package/docs/roadmap.md +88 -0
- package/docs/safety.md +147 -0
- package/docs/storage.md +93 -0
- package/package.json +1 -1
- package/docs/funcionamiento-del-sistema.md +0 -865
- package/docs/harness-gap-analysis.md +0 -243
- package/docs/vgxcode.md +0 -87
- package/docs/vgxness-code.md +0 -48
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { installOpenCodeMcpClient } from '../../mcp/client-install-opencode.js';
|
|
2
|
+
import { installClaudeCodeMcpClient } from '../../mcp/client-install-claude-code.js';
|
|
3
|
+
import { planClaudeCodeMcpInstall } from '../../mcp/client-install-claude-code-contract.js';
|
|
2
4
|
import { planOpenCodeMcpInstall } from '../../mcp/client-install-opencode-contract.js';
|
|
3
5
|
import { createMcpClientSetupPreview, isMcpClientSetupProvider } from '../../mcp/client-setup-preview.js';
|
|
4
6
|
import { createMcpDoctorReport } from '../../mcp/doctor.js';
|
|
5
7
|
import { createOpenCodeMcpVisibilityReport } from '../../mcp/opencode-visibility.js';
|
|
8
|
+
import { RunService } from '../../runs/run-service.js';
|
|
6
9
|
import { databasePathFor, databasePathSelectionFor, opencodeInstallScopeFlag, optionalNumberFlag, optionalStringFlag } from '../cli-flags.js';
|
|
7
10
|
import { usageFailure, validationFailure } from '../cli-help.js';
|
|
8
|
-
import { jsonResult, resultFailure } from '../cli-helpers.js';
|
|
11
|
+
import { jsonResult, openCliDatabase, resultFailure } from '../cli-helpers.js';
|
|
9
12
|
import { renderDoctorReport } from '../doctor-renderer.js';
|
|
10
13
|
export function runMcpInstallCommand(parsed, environment) {
|
|
11
14
|
return (async () => {
|
|
12
15
|
const client = parsed.positionals[2];
|
|
13
|
-
if (client !== 'opencode')
|
|
14
|
-
return usageFailure('mcp install requires client opencode');
|
|
16
|
+
if (client !== 'opencode' && client !== 'claude')
|
|
17
|
+
return usageFailure('mcp install requires client opencode or claude');
|
|
15
18
|
const databasePath = databasePathSelectionFor(parsed.flags, environment);
|
|
16
19
|
if (!databasePath.ok)
|
|
17
20
|
return resultFailure(databasePath);
|
|
@@ -20,6 +23,35 @@ export function runMcpInstallCommand(parsed, environment) {
|
|
|
20
23
|
return resultFailure(scope);
|
|
21
24
|
const mcpOnly = parsed.flags['mcp-only'] === true || parsed.flags['no-agents'] === true;
|
|
22
25
|
const overwriteVgxness = parsed.flags['overwrite-vgxness'] === true || parsed.flags.reinstall === true;
|
|
26
|
+
if (client === 'claude') {
|
|
27
|
+
if (parsed.flags.plan === true) {
|
|
28
|
+
return jsonResult({ ok: true, value: planClaudeCodeMcpInstall({ cwd: environment.cwd, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, overwriteVgxness }) });
|
|
29
|
+
}
|
|
30
|
+
if (parsed.flags.yes !== true) {
|
|
31
|
+
return resultFailure({ ok: false, error: { code: 'validation_failed', message: 'confirmation_required: `mcp install claude` requires explicit --yes before any project config write.' } });
|
|
32
|
+
}
|
|
33
|
+
if (optionalStringFlag(parsed.flags, 'run-id') === undefined) {
|
|
34
|
+
return resultFailure({ ok: false, error: { code: 'validation_failed', message: 'preflight_failed: Claude Code provider config writes require VGXNESS preflight; pass --run-id for an existing VGXNESS run before using --yes.' } });
|
|
35
|
+
}
|
|
36
|
+
const opened = openCliDatabase(databasePath.value.path);
|
|
37
|
+
if (!opened.ok)
|
|
38
|
+
return resultFailure(opened);
|
|
39
|
+
try {
|
|
40
|
+
const preflight = createClaudeCodeInstallPreflight(parsed, opened.value, environment);
|
|
41
|
+
const result = await installClaudeCodeMcpClient({
|
|
42
|
+
cwd: environment.cwd,
|
|
43
|
+
databasePath: databasePath.value.path,
|
|
44
|
+
databasePathSource: databasePath.value.source,
|
|
45
|
+
confirmed: true,
|
|
46
|
+
overwriteVgxness,
|
|
47
|
+
preflight,
|
|
48
|
+
});
|
|
49
|
+
return result.status === 'installed' ? jsonResult({ ok: true, value: result }) : resultFailure({ ok: false, error: { code: 'validation_failed', message: `${result.reason}: ${result.message}` } });
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
opened.value.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
23
55
|
if (parsed.flags.plan === true) {
|
|
24
56
|
return jsonResult({
|
|
25
57
|
ok: true,
|
|
@@ -55,6 +87,36 @@ export function runMcpInstallCommand(parsed, environment) {
|
|
|
55
87
|
});
|
|
56
88
|
})();
|
|
57
89
|
}
|
|
90
|
+
function createClaudeCodeInstallPreflight(parsed, database, environment) {
|
|
91
|
+
return (request) => {
|
|
92
|
+
const runId = optionalStringFlag(parsed.flags, 'run-id');
|
|
93
|
+
if (runId === undefined) {
|
|
94
|
+
return validationFailure('Claude Code provider config writes require VGXNESS preflight; pass --run-id for an existing VGXNESS run before using --yes.');
|
|
95
|
+
}
|
|
96
|
+
const service = new RunService(database);
|
|
97
|
+
const details = service.getRun(runId);
|
|
98
|
+
if (!details.ok)
|
|
99
|
+
return details;
|
|
100
|
+
const phase = optionalStringFlag(parsed.flags, 'phase') ?? details.value.phase;
|
|
101
|
+
const agentId = optionalStringFlag(parsed.flags, 'agent-id') ?? details.value.selectedAgentId;
|
|
102
|
+
const preflight = service.preflightOperation({
|
|
103
|
+
runId,
|
|
104
|
+
category: request.category,
|
|
105
|
+
operation: request.operation,
|
|
106
|
+
workspaceRoot: environment.cwd,
|
|
107
|
+
targetPath: request.targetPath,
|
|
108
|
+
providerToolName: request.providerToolName,
|
|
109
|
+
phase,
|
|
110
|
+
agentId,
|
|
111
|
+
});
|
|
112
|
+
if (!preflight.ok)
|
|
113
|
+
return preflight;
|
|
114
|
+
if (preflight.value.outcome !== 'allowed') {
|
|
115
|
+
return validationFailure(`VGXNESS preflight ${preflight.value.outcome} for Claude Code provider config write: ${request.targetPath}`);
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, value: preflight.value };
|
|
118
|
+
};
|
|
119
|
+
}
|
|
58
120
|
export function runMcpSetupCommand(parsed, environment) {
|
|
59
121
|
if (parsed.flags.preview !== true)
|
|
60
122
|
return usageFailure('mcp setup requires --preview; provider config installation is not supported');
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startVgxMcpStdioServer } from '../mcp/stdio-server.js';
|
|
3
2
|
import { dispatchCliAsync } from './dispatcher.js';
|
|
4
3
|
import { resolveMcpStartDatabasePath } from './mcp-start-path.js';
|
|
5
4
|
void main();
|
|
@@ -12,6 +11,7 @@ async function main() {
|
|
|
12
11
|
process.exitCode = 1;
|
|
13
12
|
return;
|
|
14
13
|
}
|
|
14
|
+
const { startVgxMcpStdioServer } = await import('../mcp/stdio-server.js');
|
|
15
15
|
await startVgxMcpStdioServer({ databasePath: databasePath.value });
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
@@ -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
|
-
|
|
63
|
-
|
|
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,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { projectCanonicalAgentManifestToClaudeCode } from '../agents/canonical-agent-projection.js';
|
|
4
|
+
import { assertInsideWorkspace } from './claude-code-config.js';
|
|
5
|
+
export const claudeCodeGeneratedMarker = 'VGXNESS-GENERATED';
|
|
6
|
+
export function expectedClaudeCodeAgentFiles(workspaceRoot) {
|
|
7
|
+
return projectCanonicalAgentManifestToClaudeCode().agents.map((agent) => {
|
|
8
|
+
const path = resolve(workspaceRoot, '.claude', 'agents', agent.fileName);
|
|
9
|
+
assertInsideWorkspace(workspaceRoot, path);
|
|
10
|
+
return { ...agent, path };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function renderClaudeCodeAgentMarkdown(agent) {
|
|
14
|
+
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`;
|
|
15
|
+
}
|
|
16
|
+
export function inspectClaudeCodeAgents(workspaceRoot) {
|
|
17
|
+
const directoryPath = join(workspaceRoot, '.claude', 'agents');
|
|
18
|
+
const expected = expectedClaudeCodeAgentFiles(workspaceRoot);
|
|
19
|
+
return { directoryPath, directoryExists: existsSync(directoryPath), agents: expected.map(inspectAgent) };
|
|
20
|
+
}
|
|
21
|
+
export function isVgxnessOwnedClaudeAgentMarkdown(contents) {
|
|
22
|
+
return contents.includes(claudeCodeGeneratedMarker) && contents.includes('provider=claude') && contents.includes('artifact=claude-code-subagent');
|
|
23
|
+
}
|
|
24
|
+
export function parseClaudeAgentFrontmatter(contents) {
|
|
25
|
+
if (!contents.startsWith('---\n'))
|
|
26
|
+
return { ok: false, reason: 'Missing YAML frontmatter.' };
|
|
27
|
+
const end = contents.indexOf('\n---\n', 4);
|
|
28
|
+
if (end < 0)
|
|
29
|
+
return { ok: false, reason: 'Unclosed YAML frontmatter.' };
|
|
30
|
+
const lines = contents.slice(4, end).split('\n');
|
|
31
|
+
const data = {};
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const index = line.indexOf(':');
|
|
34
|
+
if (index <= 0)
|
|
35
|
+
return { ok: false, reason: `Invalid frontmatter line: ${line}` };
|
|
36
|
+
const key = line.slice(0, index).trim();
|
|
37
|
+
const value = line.slice(index + 1).trim();
|
|
38
|
+
data[key] = unquoteYamlScalar(value);
|
|
39
|
+
}
|
|
40
|
+
if (!data.name?.trim())
|
|
41
|
+
return { ok: false, reason: 'Frontmatter name is required.' };
|
|
42
|
+
if (!data.description?.trim())
|
|
43
|
+
return { ok: false, reason: 'Frontmatter description is required.' };
|
|
44
|
+
return { ok: true, data: { name: data.name, description: data.description } };
|
|
45
|
+
}
|
|
46
|
+
function inspectAgent(agent) {
|
|
47
|
+
if (!existsSync(agent.path))
|
|
48
|
+
return { agentName: agent.name, path: agent.path, exists: false, status: 'missing', frontmatter: 'missing', generatedMarker: false, detail: 'Expected Claude agent file is missing.' };
|
|
49
|
+
try {
|
|
50
|
+
const contents = readFileSync(agent.path, 'utf8');
|
|
51
|
+
const frontmatter = parseClaudeAgentFrontmatter(contents);
|
|
52
|
+
const generatedMarker = isVgxnessOwnedClaudeAgentMarkdown(contents);
|
|
53
|
+
if (!frontmatter.ok)
|
|
54
|
+
return { agentName: agent.name, path: agent.path, exists: true, status: 'invalid', frontmatter: 'invalid', generatedMarker, detail: frontmatter.reason };
|
|
55
|
+
if (frontmatter.data.name !== agent.name)
|
|
56
|
+
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}.` };
|
|
57
|
+
if (!generatedMarker)
|
|
58
|
+
return { agentName: agent.name, path: agent.path, exists: true, status: 'conflicting', frontmatter: 'valid', generatedMarker, detail: 'Existing agent file is not marked as VGXNESS-generated.' };
|
|
59
|
+
return { agentName: agent.name, path: agent.path, exists: true, status: 'managed', frontmatter: 'valid', generatedMarker, detail: 'Claude agent file is VGXNESS-managed.' };
|
|
60
|
+
}
|
|
61
|
+
catch (cause) {
|
|
62
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
63
|
+
return { agentName: agent.name, path: agent.path, exists: true, status: 'invalid', frontmatter: 'invalid', generatedMarker: false, detail: `Unable to read Claude agent file: ${message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function yamlScalar(value) {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
}
|
|
69
|
+
function unquoteYamlScalar(value) {
|
|
70
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
71
|
+
try {
|
|
72
|
+
return value.startsWith('"') ? JSON.parse(value) : value.slice(1, -1).replaceAll("''", "'");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
@@ -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.md'), 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,86 @@
|
|
|
1
|
+
import { expectedClaudeCodeAgentFiles, inspectClaudeCodeAgents, renderClaudeCodeAgentMarkdown } from './claude-code-agent-config.js';
|
|
2
|
+
import { createClaudeCodeMcpDoctorCommand, createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, resolveClaudeCodeMcpJsonPath } from './claude-code-config.js';
|
|
3
|
+
export function planClaudeCodeMcpInstall(input) {
|
|
4
|
+
const source = input.databasePathSource ?? 'flag';
|
|
5
|
+
const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
|
|
6
|
+
const overwriteVgxness = input.overwriteVgxness === true;
|
|
7
|
+
let mcpPath;
|
|
8
|
+
try {
|
|
9
|
+
mcpPath = resolveClaudeCodeMcpJsonPath(input.cwd);
|
|
10
|
+
}
|
|
11
|
+
catch (cause) {
|
|
12
|
+
return refused(input, server, 'outside_workspace', cause instanceof Error ? cause.message : String(cause), [], [], overwriteVgxness);
|
|
13
|
+
}
|
|
14
|
+
const mcpState = inspectClaudeCodeMcpConfig(input.cwd);
|
|
15
|
+
const targets = [];
|
|
16
|
+
const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
|
|
17
|
+
if (mcpState.status === 'missing')
|
|
18
|
+
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'create' });
|
|
19
|
+
else if (mcpState.status === 'stale')
|
|
20
|
+
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'merge' });
|
|
21
|
+
else if (mcpState.status === 'configured')
|
|
22
|
+
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'update-vgxness' });
|
|
23
|
+
else
|
|
24
|
+
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'blocked', reason: mcpState.message });
|
|
25
|
+
const agentInspection = inspectClaudeCodeAgents(input.cwd);
|
|
26
|
+
for (const agent of agentInspection.agents) {
|
|
27
|
+
if (agent.status === 'missing')
|
|
28
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'create' });
|
|
29
|
+
else if (agent.status === 'managed')
|
|
30
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'update-vgxness' });
|
|
31
|
+
else
|
|
32
|
+
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, action: 'blocked', reason: agent.detail });
|
|
33
|
+
}
|
|
34
|
+
const blocked = targets.find((target) => target.action === 'blocked');
|
|
35
|
+
if (blocked !== undefined) {
|
|
36
|
+
const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : 'existing_vgxness_agent';
|
|
37
|
+
return refused(input, server, reason, blocked.reason ?? 'Claude Code install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
|
|
38
|
+
}
|
|
39
|
+
const backupRequired = targets.some((target) => (target.kind === 'mcp-json' && target.action !== 'create') || (target.kind === 'agent-file' && target.action === 'update-vgxness'));
|
|
40
|
+
return {
|
|
41
|
+
...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness),
|
|
42
|
+
status: 'would_install',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function expectedClaudeCodeRenderedAgents(workspaceRoot) {
|
|
46
|
+
return expectedClaudeCodeAgentFiles(workspaceRoot).map((agent) => ({ path: agent.path, agentName: agent.name, contents: renderClaudeCodeAgentMarkdown(agent) }));
|
|
47
|
+
}
|
|
48
|
+
function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
|
|
49
|
+
return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness), status: 'refused', reason, message };
|
|
50
|
+
}
|
|
51
|
+
function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness) {
|
|
52
|
+
const source = input.databasePathSource ?? 'flag';
|
|
53
|
+
const targetPath = resolveClaudeCodeMcpJsonPath(input.cwd);
|
|
54
|
+
return {
|
|
55
|
+
version: 1,
|
|
56
|
+
kind: 'mcp-client-install-claude-code',
|
|
57
|
+
installable: true,
|
|
58
|
+
mutating: false,
|
|
59
|
+
provider: 'claude',
|
|
60
|
+
scope: 'project',
|
|
61
|
+
targetPath,
|
|
62
|
+
targets,
|
|
63
|
+
backupRequired,
|
|
64
|
+
safety: { operation: 'plan', mutating: false, writesProviderConfig: false, targetPath, backupRequired, mergePolicy: backupRequired ? 'merge-preserve-existing' : 'create' },
|
|
65
|
+
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.',
|
|
68
|
+
],
|
|
69
|
+
verificationHints: [
|
|
70
|
+
{ kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed project config installation.' },
|
|
71
|
+
{ kind: 'manual-check', message: 'Open the project in Claude Code and verify the vgxness MCP server and project agents are visible.' },
|
|
72
|
+
{ kind: 'command', message: 'Run the MCP doctor command after installation.', command: createClaudeCodeMcpDoctorCommand(input.databasePath, source) },
|
|
73
|
+
],
|
|
74
|
+
server,
|
|
75
|
+
preservedTopLevelKeys,
|
|
76
|
+
agentNames: targets.filter((target) => target.kind === 'agent-file').map((target) => target.agentName),
|
|
77
|
+
overwriteVgxness,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function mcpRefusalReason(status) {
|
|
81
|
+
if (status === 'invalid')
|
|
82
|
+
return 'malformed_json';
|
|
83
|
+
if (status === 'conflicting')
|
|
84
|
+
return 'existing_vgxness_mcp';
|
|
85
|
+
return 'invalid_mcp_shape';
|
|
86
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { createManagedProviderConfigBackup } from '../setup/backup-rollback-service.js';
|
|
4
|
+
import { parseClaudeAgentFrontmatter } from './claude-code-agent-config.js';
|
|
5
|
+
import { createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, mergeClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
6
|
+
import { expectedClaudeCodeRenderedAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
|
|
7
|
+
export async function installClaudeCodeMcpClient(input) {
|
|
8
|
+
const source = input.databasePathSource ?? 'flag';
|
|
9
|
+
const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
|
|
10
|
+
const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}) });
|
|
11
|
+
if (!input.confirmed)
|
|
12
|
+
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
|
|
13
|
+
if (plan.status === 'refused')
|
|
14
|
+
return refusal(plan.reason, plan.message, plan, server, [], []);
|
|
15
|
+
if (input.preflight === undefined) {
|
|
16
|
+
return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any project config write.', plan, server, [], []);
|
|
17
|
+
}
|
|
18
|
+
const preflightPaths = unique(plan.targets.filter((target) => target.action !== 'blocked').map((target) => target.path));
|
|
19
|
+
for (const targetPath of preflightPaths) {
|
|
20
|
+
const preflight = await input.preflight({
|
|
21
|
+
category: 'provider-tool',
|
|
22
|
+
operation: 'write claude project provider config',
|
|
23
|
+
targetPath,
|
|
24
|
+
workspaceRoot: input.cwd,
|
|
25
|
+
providerToolName: 'claude-code',
|
|
26
|
+
...(input.runId !== undefined ? { runId: input.runId } : {}),
|
|
27
|
+
...(input.agentId !== undefined ? { agentId: input.agentId } : {}),
|
|
28
|
+
...(input.phase !== undefined ? { phase: input.phase } : {}),
|
|
29
|
+
});
|
|
30
|
+
if (!preflight.ok)
|
|
31
|
+
return refusal('preflight_failed', preflight.error.message, plan, server, [], []);
|
|
32
|
+
}
|
|
33
|
+
const backups = [];
|
|
34
|
+
const writtenPaths = [];
|
|
35
|
+
const existingTargets = plan.targets.filter((target) => target.action === 'merge' || target.action === 'update-vgxness');
|
|
36
|
+
for (const target of existingTargets) {
|
|
37
|
+
const backup = createBackup(target.path);
|
|
38
|
+
if (!backup.ok)
|
|
39
|
+
return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
|
|
40
|
+
backups.push(toBackupSummary(backup.value));
|
|
41
|
+
}
|
|
42
|
+
const mcpWrite = writeMcpJson(input.cwd, plan, server);
|
|
43
|
+
if (!mcpWrite.ok)
|
|
44
|
+
return refusal('post_write_validation_failed', mcpWrite.error.message, plan, server, writtenPaths, backups);
|
|
45
|
+
writtenPaths.push(mcpWrite.value);
|
|
46
|
+
for (const agent of expectedClaudeCodeRenderedAgents(input.cwd)) {
|
|
47
|
+
const target = plan.targets.find((item) => item.kind === 'agent-file' && item.path === agent.path);
|
|
48
|
+
if (target?.action !== 'create' && target?.action !== 'update-vgxness')
|
|
49
|
+
continue;
|
|
50
|
+
mkdirSync(dirname(agent.path), { recursive: true });
|
|
51
|
+
writeFileSync(agent.path, agent.contents);
|
|
52
|
+
const validation = parseClaudeAgentFrontmatter(readFileSync(agent.path, 'utf8'));
|
|
53
|
+
if (!validation.ok || validation.data.name !== agent.agentName)
|
|
54
|
+
return refusal('post_write_validation_failed', `Claude agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
|
|
55
|
+
writtenPaths.push(agent.path);
|
|
56
|
+
}
|
|
57
|
+
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'installed', targetPath: plan.targetPath, writtenPaths, backups, safety: applySafety(plan), server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
|
|
58
|
+
}
|
|
59
|
+
function writeMcpJson(cwd, plan, server) {
|
|
60
|
+
const state = inspectClaudeCodeMcpConfig(cwd);
|
|
61
|
+
if (state.status === 'invalid' || state.status === 'conflicting')
|
|
62
|
+
return { ok: false, error: { code: 'validation_failed', message: state.message } };
|
|
63
|
+
if (plan.targets.find((target) => target.kind === 'mcp-json')?.action === 'create' && existsSync(plan.targetPath))
|
|
64
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude .mcp.json appeared after planning; rerun apply to merge safely.' } };
|
|
65
|
+
const merged = mergeClaudeCodeMcpConfig(state.parsed ? state.config : {}, server);
|
|
66
|
+
mkdirSync(dirname(plan.targetPath), { recursive: true });
|
|
67
|
+
writeFileSync(plan.targetPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
68
|
+
const after = inspectClaudeCodeMcpConfig(cwd);
|
|
69
|
+
return after.status === 'configured' ? { ok: true, value: plan.targetPath } : { ok: false, error: { code: 'validation_failed', message: 'Claude .mcp.json did not validate after write.' } };
|
|
70
|
+
}
|
|
71
|
+
function createBackup(path) {
|
|
72
|
+
return createManagedProviderConfigBackup({ targetPath: path, provider: 'claude', scope: 'project', createdByOperation: 'mcp-client-install-claude-code', reason: 'pre-merge-safety', description: 'Backup existing Claude Code project config before merging VGXNESS MCP or agent configuration.' });
|
|
73
|
+
}
|
|
74
|
+
function refusal(reason, message, plan, server, writtenPaths, backups) {
|
|
75
|
+
return { version: 1, kind: 'mcp-client-install-claude-code', status: 'refused', reason, message, targetPath: plan.targetPath, writtenPaths, backups, safety: { ...plan.safety, operation: 'apply', mutating: false, writesProviderConfig: false }, server, warnings: plan.warnings, verificationHints: plan.verificationHints, agentNames: plan.agentNames, overwriteVgxness: plan.overwriteVgxness };
|
|
76
|
+
}
|
|
77
|
+
function applySafety(plan) {
|
|
78
|
+
return { ...plan.safety, operation: 'apply', mutating: true, writesProviderConfig: true };
|
|
79
|
+
}
|
|
80
|
+
function toBackupSummary(backup) {
|
|
81
|
+
return { kind: 'managed', backupId: backup.backupId, backupPath: backup.backupPath, metadataPath: backup.metadataPath, scope: backup.scope, byteSize: backup.byteSize, contentHash: backup.contentHash };
|
|
82
|
+
}
|
|
83
|
+
function unique(values) {
|
|
84
|
+
return [...new Set(values)];
|
|
85
|
+
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -2,12 +2,17 @@ export * from '../providers/opencode/manager-payload.js';
|
|
|
2
2
|
export * from '../verification/index.js';
|
|
3
3
|
export * from './client-install-opencode.js';
|
|
4
4
|
export * from './client-install-opencode-contract.js';
|
|
5
|
+
export * from './client-install-claude-code.js';
|
|
6
|
+
export * from './client-install-claude-code-contract.js';
|
|
5
7
|
export * from './client-setup-preview.js';
|
|
8
|
+
export * from './claude-code-agent-config.js';
|
|
9
|
+
export * from './claude-code-config.js';
|
|
6
10
|
export * from './control-plane.js';
|
|
7
11
|
export * from './doctor.js';
|
|
8
12
|
export * from './opencode-visibility.js';
|
|
9
13
|
export * from './opencode-handoff-preview.js';
|
|
10
14
|
export * from './provider-change-plan.js';
|
|
15
|
+
export * from './provider-canonical-agent-manifest.js';
|
|
11
16
|
export * from './provider-doctor.js';
|
|
12
17
|
export * from './provider-health-types.js';
|
|
13
18
|
export * from './provider-status.js';
|