vgxness 1.9.8 → 1.10.1
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 +5 -4
- package/dist/cli/cli-flags.js +15 -2
- package/dist/cli/cli-help.js +13 -13
- package/dist/cli/commands/mcp-dispatcher.js +8 -4
- package/dist/cli/commands/setup-dispatcher.js +4 -4
- package/dist/cli/doctor-renderer.js +1 -1
- package/dist/cli/setup-plan-renderer.js +2 -2
- package/dist/cli/setup-status-renderer.js +2 -2
- package/dist/cli/tui/main-menu/main-menu-read-model.js +28 -28
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +5 -1
- package/dist/cli/tui/opentui/main-menu/view.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +23 -23
- package/dist/cli/tui/setup/setup-tui-render-shape.js +15 -12
- package/dist/cli/tui/setup/setup-tui-state.js +1 -3
- package/dist/cli/tui/setup/setup-tui-view-helpers.js +18 -2
- package/dist/mcp/claude-code-config.js +2 -0
- package/dist/mcp/claude-code-scope.js +1 -0
- package/dist/mcp/claude-code-user-config.js +2 -0
- package/dist/mcp/client-install-claude-code-contract.js +15 -72
- package/dist/mcp/client-install-claude-code.js +4 -6
- package/dist/mcp/client-install-opencode-contract.js +5 -79
- package/dist/mcp/opencode-visibility.js +4 -3
- package/dist/mcp/provider-change-plan.js +17 -14
- package/dist/mcp/provider-doctor.js +38 -20
- package/dist/mcp/provider-health-types.js +19 -0
- package/dist/mcp/provider-status.js +29 -18
- package/dist/mcp/schema.js +5 -4
- package/dist/mcp/validation.js +11 -1
- package/dist/setup/backup-rollback-service.js +33 -2
- package/dist/setup/providers/claude-setup-adapter.js +13 -15
- package/dist/setup/providers/opencode-setup-adapter.js +12 -12
- package/dist/setup/setup-defaults.js +1 -0
- package/dist/setup/setup-plan.js +6 -6
- package/docs/architecture.md +3 -3
- package/docs/cli.md +29 -33
- package/docs/glossary.md +2 -2
- package/docs/mcp.md +1 -1
- package/docs/prd.md +3 -3
- package/docs/providers.md +14 -15
- package/docs/roadmap.md +1 -1
- package/docs/safety.md +1 -1
- package/docs/storage.md +1 -1
- package/package.json +1 -1
|
@@ -12,8 +12,8 @@ function screenLines(input, width) {
|
|
|
12
12
|
'Guided installation sequence',
|
|
13
13
|
'Setup welcome: global memory → provider → OpenCode details → agent readiness → preview → final confirmation → doctor/restart.',
|
|
14
14
|
'VGXNESS Setup Assistant',
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
`Configure local storage and OpenCode integration with read-only previews first ${badgeLabels.readOnly} ${badgeLabels.preview}.`,
|
|
16
|
+
`No provider config is written until the final confirmation ${badgeLabels.noFilesWritten}.`,
|
|
17
17
|
], width, [setupFooterHints.continue, setupFooterHints.help, setupFooterHints.cancel]);
|
|
18
18
|
case 'project-database':
|
|
19
19
|
return workspaceLines(vm, [
|
|
@@ -32,12 +32,12 @@ function screenLines(input, width) {
|
|
|
32
32
|
'Provider selection',
|
|
33
33
|
...choiceLines(vm.providerChoices),
|
|
34
34
|
vm.providerInstallabilityLabel,
|
|
35
|
-
|
|
35
|
+
`OpenCode setup remains gated by final confirmation ${badgeLabels.confirmRequired}. Manual / none has no automatic install and no provider config will be written.`,
|
|
36
36
|
], width, choiceFooter());
|
|
37
37
|
case 'opencode-details':
|
|
38
38
|
return workspaceLines(vm, vm.providerLabel === 'OpenCode'
|
|
39
39
|
? [
|
|
40
|
-
'OpenCode plan ' + badgeLabels.
|
|
40
|
+
'OpenCode plan ' + badgeLabels.confirmRequired,
|
|
41
41
|
...choiceLines(vm.scopeChoices),
|
|
42
42
|
...choiceLines(vm.installModeChoices),
|
|
43
43
|
...choiceLines(vm.overwriteChoices),
|
|
@@ -47,7 +47,7 @@ function screenLines(input, width) {
|
|
|
47
47
|
`Action: ${vm.opencodeActionLabel}`,
|
|
48
48
|
`Agent readiness: ${vm.agentReadinessLabel}`,
|
|
49
49
|
vm.agentReadinessDetail,
|
|
50
|
-
|
|
50
|
+
`Details are preview-only here ${badgeLabels.preview} ${badgeLabels.noFilesWritten}; writes require final confirmation.`,
|
|
51
51
|
]
|
|
52
52
|
: [
|
|
53
53
|
'Manual / no-provider-write mode ' + badgeLabels.manual,
|
|
@@ -57,19 +57,22 @@ function screenLines(input, width) {
|
|
|
57
57
|
], width, vm.providerLabel === 'OpenCode' ? choiceFooter() : [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]);
|
|
58
58
|
case 'plan-review':
|
|
59
59
|
return workspaceLines(vm, [
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
`Read-only plan review ${badgeLabels.preview}`,
|
|
61
|
+
`Plan summary ${badgeLabels.readOnly} ${badgeLabels.noFilesWritten}`,
|
|
62
|
+
'Plan generated. No files were written.',
|
|
62
63
|
...planLines(vm, width),
|
|
63
64
|
...nextCommandLines(vm),
|
|
64
|
-
|
|
65
|
+
`Review only: this screen does not write provider config ${badgeLabels.noFilesWritten}.`,
|
|
65
66
|
vm.canAutoApply ? 'Press Enter to continue to final confirmation.' : 'Press Enter to show manual next steps; no automatic apply is available.',
|
|
66
67
|
], width, vm.canAutoApply ? [setupFooterHints.finalConfirmation, setupFooterHints.back, setupFooterHints.cancel] : [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]);
|
|
67
68
|
case 'final-confirmation':
|
|
68
69
|
return workspaceLines(vm, [
|
|
69
70
|
'Final confirmation',
|
|
70
|
-
badgeLabels.warning
|
|
71
|
+
`${badgeLabels.warning} ${badgeLabels.confirmRequired}`,
|
|
71
72
|
'Confirm OpenCode setup',
|
|
72
73
|
...planLines(vm, width),
|
|
74
|
+
'Explicit confirmation is required before writing provider config.',
|
|
75
|
+
'This is the first write-capable screen; earlier wizard steps were preview/read-only only.',
|
|
73
76
|
'WARNING: OpenCode provider config may be modified at the target path above. Existing config is backed up when planned.',
|
|
74
77
|
vm.safetyWarning,
|
|
75
78
|
'After confirmation: run doctor, then restart OpenCode to reload MCP configuration.',
|
|
@@ -84,12 +87,12 @@ function screenLines(input, width) {
|
|
|
84
87
|
'Setup recovery',
|
|
85
88
|
badgeLabels.error,
|
|
86
89
|
`Error: ${input.error ?? 'Unknown setup error'}`,
|
|
87
|
-
|
|
90
|
+
`No unconfirmed provider config write was performed ${badgeLabels.noFilesWritten}.`,
|
|
88
91
|
'Next: inspect `vgxness setup plan`, resolve blockers, then retry.',
|
|
89
92
|
footer([setupFooterHints.close], width),
|
|
90
93
|
];
|
|
91
94
|
case 'cancelled':
|
|
92
|
-
return ['Setup cancelled', badgeLabels.cancelled
|
|
95
|
+
return ['Setup cancelled', `${badgeLabels.cancelled} ${badgeLabels.noFilesWritten}`, 'Cancelled. No changes were applied.', 'No provider config was written.', 'No agent seeding was performed.', footer([setupFooterHints.close], width)];
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
function planLines(vm, width) {
|
|
@@ -153,7 +156,7 @@ function resultLines(result, vm, width) {
|
|
|
153
156
|
...vm.nextCommands.map((command) => `Next: ${command}`),
|
|
154
157
|
footer([setupFooterHints.close], width),
|
|
155
158
|
];
|
|
156
|
-
return ['Setup result',
|
|
159
|
+
return ['Setup result', `${badgeLabels.noFilesWritten} No provider config write occurred.`, footer([setupFooterHints.close], width)];
|
|
157
160
|
}
|
|
158
161
|
function resultNextCommands(vm) {
|
|
159
162
|
const filtered = vm.nextCommands.filter((command) => !/^vgx(?:ness)? setup apply\b/.test(command));
|
|
@@ -4,7 +4,7 @@ const screenChoiceIds = {
|
|
|
4
4
|
welcome: [],
|
|
5
5
|
'project-database': ['database:global', 'database:project-local', 'database:custom'],
|
|
6
6
|
provider: ['provider:opencode', 'provider:none'],
|
|
7
|
-
'opencode-details': ['scope:
|
|
7
|
+
'opencode-details': ['scope:user', 'install:mcp-plus-agents', 'install:mcp-only', 'overwrite:vgxness'],
|
|
8
8
|
'plan-review': [],
|
|
9
9
|
'final-confirmation': [],
|
|
10
10
|
applying: [],
|
|
@@ -126,8 +126,6 @@ function selectFocusedChoice(state) {
|
|
|
126
126
|
return reduceSetupTuiState(state, { type: 'select-provider', provider: 'opencode' });
|
|
127
127
|
case 'provider:none':
|
|
128
128
|
return reduceSetupTuiState(state, { type: 'select-provider', provider: 'none' });
|
|
129
|
-
case 'scope:project':
|
|
130
|
-
return reduceSetupTuiState(state, { type: 'select-scope', scope: 'project' });
|
|
131
129
|
case 'scope:user':
|
|
132
130
|
return reduceSetupTuiState(state, { type: 'select-scope', scope: 'user' });
|
|
133
131
|
case 'install:mcp-plus-agents':
|
|
@@ -3,10 +3,14 @@ export const badgeLabels = {
|
|
|
3
3
|
recommended: '[recommended]',
|
|
4
4
|
selected: '[selected]',
|
|
5
5
|
focused: '[focused]',
|
|
6
|
+
preview: '[preview]',
|
|
6
7
|
warning: '[warning]',
|
|
7
8
|
error: '[error]',
|
|
8
|
-
|
|
9
|
+
confirmRequired: '[confirm required]',
|
|
10
|
+
writeAfterConfirm: '[confirm required]',
|
|
11
|
+
blocked: '[blocked]',
|
|
9
12
|
readOnly: '[read-only]',
|
|
13
|
+
noFilesWritten: '[no files written]',
|
|
10
14
|
manual: '[manual]',
|
|
11
15
|
deferred: '[deferred]',
|
|
12
16
|
cancelled: '[cancelled]',
|
|
@@ -35,7 +39,19 @@ export function compactPath(path, width) {
|
|
|
35
39
|
export function wrapLabel(value, width) {
|
|
36
40
|
if (width <= 0 || value.length <= width)
|
|
37
41
|
return value;
|
|
38
|
-
const protectedPhrases = [
|
|
42
|
+
const protectedPhrases = [
|
|
43
|
+
'final confirmation',
|
|
44
|
+
'confirm and apply',
|
|
45
|
+
'not installable',
|
|
46
|
+
'unrelated config is preserved',
|
|
47
|
+
'read-only',
|
|
48
|
+
'confirm required',
|
|
49
|
+
'no files written',
|
|
50
|
+
'requires confirmation',
|
|
51
|
+
'will write after confirm',
|
|
52
|
+
'error',
|
|
53
|
+
'cancelled',
|
|
54
|
+
];
|
|
39
55
|
const phrase = protectedPhrases.find((candidate) => value.includes(candidate));
|
|
40
56
|
if (phrase !== undefined)
|
|
41
57
|
return `${value.slice(0, Math.max(0, width - phrase.length - 5)).trim()} ... ${phrase}`.trim();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { providerConfigPathDiagnostics } from './provider-health-types.js';
|
|
3
4
|
export function resolveClaudeCodeMcpJsonPath(workspaceRoot) {
|
|
4
5
|
const target = resolve(workspaceRoot, '.mcp.json');
|
|
5
6
|
assertInsideWorkspace(workspaceRoot, target);
|
|
@@ -55,6 +56,7 @@ export function claudeMcpConfigPathStatus(state) {
|
|
|
55
56
|
parsed: state.parsed,
|
|
56
57
|
status: state.status === 'configured' ? 'pass' : state.status === 'invalid' || state.status === 'conflicting' ? 'fail' : 'not-configured',
|
|
57
58
|
detail: state.message,
|
|
59
|
+
diagnostics: providerConfigPathDiagnostics('external-project', state.exists),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
export function claudeMcpEntryStatus(state) {
|
|
@@ -16,3 +16,4 @@ export function resolveClaudeCodeScope(input, fallback = 'project') {
|
|
|
16
16
|
export function isClaudeCodeUserScope(scope) {
|
|
17
17
|
return scope === 'user';
|
|
18
18
|
}
|
|
19
|
+
export const CLAUDE_CODE_USER_GLOBAL_ONLY_MESSAGE = 'VGX-managed Claude provider configuration is user-global only. Project/local Claude files are treated as external/manual diagnostics and will not be written by VGXNESS. Re-run without the project/local install scope.';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { providerConfigPathDiagnostics } from './provider-health-types.js';
|
|
3
4
|
import { isManagedClaudeCodeMcpServer } from './claude-code-config.js';
|
|
4
5
|
import { safeHomeDirectory } from './claude-code-agent-config.js';
|
|
5
6
|
export function resolveClaudeCodeUserMcpJsonPath(env = process.env) {
|
|
@@ -39,6 +40,7 @@ export function claudeUserMcpConfigPathStatus(state) {
|
|
|
39
40
|
parsed: state.parsed,
|
|
40
41
|
status: state.status === 'configured' ? 'pass' : state.status === 'invalid' || state.status === 'conflicting' ? 'fail' : 'not-configured',
|
|
41
42
|
detail: state.message,
|
|
43
|
+
diagnostics: providerConfigPathDiagnostics('managed-user-global', state.exists),
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
export function claudeUserMcpEntryStatus(state) {
|
|
@@ -1,68 +1,23 @@
|
|
|
1
1
|
import { withEffectiveManagerInstructions } from '../agents/canonical-agent-projection.js';
|
|
2
2
|
import { expectedClaudeCodeAgentFiles, inspectClaudeCodeAgents, renderClaudeCodeAgentMarkdown } from './claude-code-agent-config.js';
|
|
3
3
|
import { buildClaudeCodeMcpAddCommand } from './claude-code-cli.js';
|
|
4
|
-
import { createClaudeCodeMcpDoctorCommand, createClaudeCodeMcpServerConfig
|
|
5
|
-
import {
|
|
6
|
-
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
4
|
+
import { createClaudeCodeMcpDoctorCommand, createClaudeCodeMcpServerConfig } from './claude-code-config.js';
|
|
5
|
+
import { CLAUDE_CODE_USER_GLOBAL_ONLY_MESSAGE, resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
7
6
|
import { inspectClaudeCodeUserMcpConfig, resolveClaudeCodeUserMcpJsonPath } from './claude-code-user-config.js';
|
|
8
7
|
import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
9
8
|
export function planClaudeCodeMcpInstall(input) {
|
|
10
9
|
const source = input.databasePathSource ?? 'flag';
|
|
11
10
|
const server = createClaudeCodeMcpServerConfig(input.databasePath, source);
|
|
12
11
|
const overwriteVgxness = input.overwriteVgxness === true;
|
|
13
|
-
const resolvedScope = resolveClaudeCodeScope(input.scope);
|
|
12
|
+
const resolvedScope = resolveClaudeCodeScope(input.scope, 'user');
|
|
14
13
|
if (!resolvedScope.ok)
|
|
15
14
|
return refused(input, server, 'unsupported_scope', resolvedScope.error.message, [], [], overwriteVgxness);
|
|
15
|
+
if (resolvedScope.value.canonical !== 'user')
|
|
16
|
+
return refused(input, server, 'unsupported_scope', CLAUDE_CODE_USER_GLOBAL_ONLY_MESSAGE, [], [], overwriteVgxness);
|
|
16
17
|
const cliCommand = buildClaudeCodeMcpAddCommand({ scope: resolvedScope.value.canonical });
|
|
17
18
|
if (!cliCommand.ok)
|
|
18
19
|
return refused(input, server, 'unsupported_scope', cliCommand.error.message, [], [], overwriteVgxness);
|
|
19
|
-
|
|
20
|
-
if (resolvedScope.value.canonical === 'local') {
|
|
21
|
-
const targets = [{ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' }];
|
|
22
|
-
return { ...base(input, server, targets, [], false, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings), status: 'would_install' };
|
|
23
|
-
}
|
|
24
|
-
return planUserInstall(input, server, overwriteVgxness, cliCommand.value, resolvedScope.value.warnings);
|
|
25
|
-
}
|
|
26
|
-
let mcpPath;
|
|
27
|
-
try {
|
|
28
|
-
mcpPath = resolveClaudeCodeMcpJsonPath(input.cwd);
|
|
29
|
-
}
|
|
30
|
-
catch (cause) {
|
|
31
|
-
return refused(input, server, 'outside_workspace', cause instanceof Error ? cause.message : String(cause), [], [], overwriteVgxness);
|
|
32
|
-
}
|
|
33
|
-
const mcpState = inspectClaudeCodeMcpConfig(input.cwd);
|
|
34
|
-
const targets = [];
|
|
35
|
-
targets.push({ kind: 'cli-mcp-registration', scope: resolvedScope.value.canonical, command: cliCommand.value, action: 'register' });
|
|
36
|
-
const preservedTopLevelKeys = mcpState.parsed ? Object.keys(mcpState.config) : [];
|
|
37
|
-
if (mcpState.status === 'missing')
|
|
38
|
-
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'create' });
|
|
39
|
-
else if (mcpState.status === 'stale')
|
|
40
|
-
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'merge' });
|
|
41
|
-
else if (mcpState.status === 'configured')
|
|
42
|
-
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'update-vgxness' });
|
|
43
|
-
else
|
|
44
|
-
targets.push({ kind: 'mcp-json', path: mcpPath, action: 'blocked', reason: mcpState.message });
|
|
45
|
-
const agentInspection = inspectClaudeCodeAgents(input.cwd);
|
|
46
|
-
for (const agent of agentInspection.agents) {
|
|
47
|
-
if (agent.status === 'missing')
|
|
48
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'create' });
|
|
49
|
-
else if (agent.status === 'managed')
|
|
50
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'update-vgxness' });
|
|
51
|
-
else
|
|
52
|
-
targets.push({ kind: 'agent-file', path: agent.path, agentName: agent.agentName, scope: 'project', external: false, action: 'blocked', reason: agent.detail });
|
|
53
|
-
}
|
|
54
|
-
const projectMemory = inspectClaudeProjectMemory(input.cwd);
|
|
55
|
-
targets.push({ kind: 'project-memory', path: projectMemory.path, action: projectMemory.action, status: projectMemory.status, backupRequired: projectMemory.backupRequired, ...(projectMemory.status === 'blocked' ? { reason: projectMemory.message } : {}) });
|
|
56
|
-
const blocked = targets.find((target) => target.action === 'blocked');
|
|
57
|
-
if (blocked !== undefined) {
|
|
58
|
-
const reason = blocked.kind === 'mcp-json' ? mcpRefusalReason(mcpState.status) : blocked.kind === 'project-memory' ? projectMemoryRefusalReason(projectMemory) : 'existing_vgxness_agent';
|
|
59
|
-
return refused(input, server, reason, blocked.reason ?? 'Claude Code install plan is blocked by an existing conflicting target.', targets, preservedTopLevelKeys, overwriteVgxness);
|
|
60
|
-
}
|
|
61
|
-
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));
|
|
62
|
-
return {
|
|
63
|
-
...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, resolvedScope.value.canonical, cliCommand.value, resolvedScope.value.warnings),
|
|
64
|
-
status: 'would_install',
|
|
65
|
-
};
|
|
20
|
+
return planUserInstall(input, server, overwriteVgxness, cliCommand.value, resolvedScope.value.warnings);
|
|
66
21
|
}
|
|
67
22
|
export function expectedClaudeCodeRenderedAgents(workspaceRoot, options) {
|
|
68
23
|
const files = expectedClaudeCodeAgentFiles(workspaceRoot);
|
|
@@ -116,21 +71,21 @@ function planUserInstall(input, server, overwriteVgxness, cliCommand, scopeWarni
|
|
|
116
71
|
return { ...base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, 'user', cliCommand, scopeWarnings), status: 'would_install' };
|
|
117
72
|
}
|
|
118
73
|
function refused(input, server, reason, message, targets, preservedTopLevelKeys, overwriteVgxness) {
|
|
119
|
-
const resolved = resolveClaudeCodeScope(input.scope);
|
|
120
|
-
const canonical = resolved.ok ? resolved.value.canonical : '
|
|
121
|
-
const cli = buildClaudeCodeMcpAddCommand({ scope: canonical });
|
|
122
|
-
return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness, canonical, cli
|
|
74
|
+
const resolved = resolveClaudeCodeScope(input.scope, 'user');
|
|
75
|
+
const canonical = resolved.ok ? resolved.value.canonical : 'user';
|
|
76
|
+
const cli = canonical === 'user' ? buildClaudeCodeMcpAddCommand({ scope: canonical }) : undefined;
|
|
77
|
+
return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness, canonical, cli?.ok ? cli.value : undefined, resolved.ok ? resolved.value.warnings : []), status: 'refused', reason, message };
|
|
123
78
|
}
|
|
124
79
|
function base(input, server, targets, preservedTopLevelKeys, backupRequired, overwriteVgxness, canonicalClaudeScope, cliCommand, scopeWarnings) {
|
|
125
80
|
const source = input.databasePathSource ?? 'flag';
|
|
126
|
-
const targetPath =
|
|
81
|
+
const targetPath = resolveClaudeCodeUserMcpJsonPath(input.env);
|
|
127
82
|
return {
|
|
128
83
|
version: 1,
|
|
129
84
|
kind: 'mcp-client-install-claude-code',
|
|
130
85
|
installable: true,
|
|
131
86
|
mutating: false,
|
|
132
87
|
provider: 'claude',
|
|
133
|
-
scope: input.scope ?? '
|
|
88
|
+
scope: input.scope ?? 'user',
|
|
134
89
|
targetPath,
|
|
135
90
|
targets,
|
|
136
91
|
backupRequired,
|
|
@@ -138,12 +93,12 @@ function base(input, server, targets, preservedTopLevelKeys, backupRequired, ove
|
|
|
138
93
|
warnings: [
|
|
139
94
|
...scopeWarnings,
|
|
140
95
|
'Claude Code MCP registration is modeled as structured config/argv, never shell strings.',
|
|
141
|
-
'
|
|
96
|
+
'VGX-managed Claude provider configuration is user-global only; project/local Claude files are treated as external/manual diagnostics and are not written by VGXNESS.',
|
|
142
97
|
'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.',
|
|
143
98
|
],
|
|
144
99
|
verificationHints: [
|
|
145
|
-
{ kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed
|
|
146
|
-
{ kind: 'manual-check', message: 'Open
|
|
100
|
+
{ kind: 'restart-client', message: 'Restart or reload Claude Code after confirmed user-global config installation.' },
|
|
101
|
+
{ kind: 'manual-check', message: 'Open Claude Code and verify the vgxness MCP server and user agents are visible.' },
|
|
147
102
|
{ kind: 'command', message: 'Run the MCP doctor command after installation.', command: createClaudeCodeMcpDoctorCommand(input.databasePath, source) },
|
|
148
103
|
],
|
|
149
104
|
server,
|
|
@@ -154,13 +109,6 @@ function base(input, server, targets, preservedTopLevelKeys, backupRequired, ove
|
|
|
154
109
|
overwriteVgxness,
|
|
155
110
|
};
|
|
156
111
|
}
|
|
157
|
-
function mcpRefusalReason(status) {
|
|
158
|
-
if (status === 'invalid')
|
|
159
|
-
return 'malformed_json';
|
|
160
|
-
if (status === 'conflicting')
|
|
161
|
-
return 'existing_vgxness_mcp';
|
|
162
|
-
return 'invalid_mcp_shape';
|
|
163
|
-
}
|
|
164
112
|
function userMcpRefusalReason(status) {
|
|
165
113
|
if (status === 'invalid')
|
|
166
114
|
return 'malformed_json';
|
|
@@ -168,11 +116,6 @@ function userMcpRefusalReason(status) {
|
|
|
168
116
|
return 'existing_vgxness_mcp';
|
|
169
117
|
return 'invalid_mcp_shape';
|
|
170
118
|
}
|
|
171
|
-
function projectMemoryRefusalReason(state) {
|
|
172
|
-
if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
|
|
173
|
-
return 'conflicting_claude_project_memory';
|
|
174
|
-
return 'malformed_claude_project_memory';
|
|
175
|
-
}
|
|
176
119
|
function userMemoryRefusalReason(state) {
|
|
177
120
|
if (state.status === 'blocked' && state.reason === 'conflicting_ownership')
|
|
178
121
|
return 'conflicting_claude_project_memory';
|
|
@@ -15,17 +15,15 @@ export async function installClaudeCodeMcpClient(input) {
|
|
|
15
15
|
if (plan.status === 'refused')
|
|
16
16
|
return refusal(plan.reason, plan.message, plan, server, [], []);
|
|
17
17
|
if (!input.confirmed)
|
|
18
|
-
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any
|
|
18
|
+
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any user-global provider config write.', plan, server, [], []);
|
|
19
19
|
if (input.preflight === undefined) {
|
|
20
|
-
return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any
|
|
20
|
+
return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any user-global provider config write.', plan, server, [], []);
|
|
21
21
|
}
|
|
22
|
-
const preflightPaths = unique(plan.targets.flatMap((target) => (isMutatingTarget(target) ? [target.path] : [])));
|
|
23
|
-
if (plan.canonicalClaudeScope !== 'project')
|
|
24
|
-
preflightPaths.unshift(plan.targetPath);
|
|
22
|
+
const preflightPaths = unique([plan.targetPath, ...plan.targets.flatMap((target) => (isMutatingTarget(target) ? [target.path] : []))]);
|
|
25
23
|
for (const targetPath of preflightPaths) {
|
|
26
24
|
const preflight = await input.preflight({
|
|
27
25
|
category: 'provider-tool',
|
|
28
|
-
operation:
|
|
26
|
+
operation: 'write claude user-global provider config',
|
|
29
27
|
targetPath,
|
|
30
28
|
workspaceRoot: input.cwd,
|
|
31
29
|
providerToolName: 'claude-code',
|
|
@@ -2,14 +2,15 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, } from './opencode-default-agent-config.js';
|
|
4
4
|
const opencodeConfigSchema = 'https://opencode.ai/config.json';
|
|
5
|
-
const
|
|
5
|
+
const opencodeUserGlobalOnlyMessage = 'VGX-managed OpenCode configuration is user-global only. Project/local OpenCode files are treated as external/manual diagnostics and will not be written by VGXNESS. Re-run without the project/local install scope.';
|
|
6
6
|
export function resolveOpenCodeMcpInstallTarget(input) {
|
|
7
7
|
const scope = input.scope ?? 'user';
|
|
8
8
|
if (scope === 'project')
|
|
9
9
|
return {
|
|
10
|
-
ok:
|
|
10
|
+
ok: false,
|
|
11
11
|
scope,
|
|
12
|
-
|
|
12
|
+
reason: 'legacy_project_scope',
|
|
13
|
+
message: opencodeUserGlobalOnlyMessage,
|
|
13
14
|
};
|
|
14
15
|
const home = input.env?.HOME;
|
|
15
16
|
if (typeof home !== 'string' || home.trim().length === 0) {
|
|
@@ -38,73 +39,7 @@ export function planOpenCodeMcpInstall(input) {
|
|
|
38
39
|
if (scope === 'user') {
|
|
39
40
|
return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
.map((relativePath) => ({
|
|
43
|
-
relativePath,
|
|
44
|
-
absolutePath: join(input.cwd, relativePath),
|
|
45
|
-
}))
|
|
46
|
-
.filter((target) => existsSync(target.absolutePath));
|
|
47
|
-
if (existingTargets.length > 1) {
|
|
48
|
-
return refusal('ambiguous_target', `Multiple OpenCode project config targets exist: ${existingTargets.map((target) => target.relativePath).join(', ')}. Remove ambiguity before installing.`, input.databasePath, databasePathSource, scope, undefined, [
|
|
49
|
-
{
|
|
50
|
-
kind: 'manual-check',
|
|
51
|
-
message: 'Remove ambiguity by keeping exactly one OpenCode project config target before installing.',
|
|
52
|
-
},
|
|
53
|
-
], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
54
|
-
}
|
|
55
|
-
if (existingTargets.length === 0) {
|
|
56
|
-
return {
|
|
57
|
-
...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
58
|
-
status: 'would_install',
|
|
59
|
-
action: 'create',
|
|
60
|
-
targetPath: join(input.cwd, '.opencode', 'opencode.json'),
|
|
61
|
-
backupRequired: false,
|
|
62
|
-
server,
|
|
63
|
-
preservedTopLevelKeys: createdConfigKeys(agentPlan),
|
|
64
|
-
existingSchema: null,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const [target] = existingTargets;
|
|
68
|
-
if (target === undefined)
|
|
69
|
-
return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness, bashPermissionPolicy);
|
|
70
|
-
if (target.relativePath.endsWith('.jsonc')) {
|
|
71
|
-
return refusal('unsupported_jsonc', `OpenCode JSONC config ${target.relativePath} is not supported yet; use JSON or remove comments first.`, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
72
|
-
}
|
|
73
|
-
const parsed = parseConfig(target.absolutePath);
|
|
74
|
-
if (!parsed.ok)
|
|
75
|
-
return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
76
|
-
const config = parsed.value;
|
|
77
|
-
if (config.mcp !== undefined && !isRecord(config.mcp)) {
|
|
78
|
-
return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
79
|
-
}
|
|
80
|
-
if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
|
|
81
|
-
return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
82
|
-
}
|
|
83
|
-
const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
|
|
84
|
-
if (agentConflict.length > 0 && !overwriteVgxness) {
|
|
85
|
-
return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${agentConflict.join(', ')}. Remove, rename, or manually reconcile them before installing.`, input.databasePath, databasePathSource, scope, target.absolutePath, [
|
|
86
|
-
{
|
|
87
|
-
kind: 'manual-check',
|
|
88
|
-
message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
|
|
89
|
-
},
|
|
90
|
-
], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
91
|
-
}
|
|
92
|
-
if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
|
|
93
|
-
return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
94
|
-
}
|
|
95
|
-
if (config.permission !== undefined && !isRecord(config.permission)) {
|
|
96
|
-
return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy),
|
|
100
|
-
status: 'would_install',
|
|
101
|
-
action: 'merge',
|
|
102
|
-
targetPath: target.absolutePath,
|
|
103
|
-
backupRequired: true,
|
|
104
|
-
server,
|
|
105
|
-
preservedTopLevelKeys: Object.keys(config),
|
|
106
|
-
existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
|
|
107
|
-
};
|
|
42
|
+
return refusal('unsupported_config_shape', opencodeUserGlobalOnlyMessage, input.databasePath, databasePathSource, scope, undefined, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
|
|
108
43
|
}
|
|
109
44
|
function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy) {
|
|
110
45
|
const target = resolveOpenCodeMcpInstallTarget({
|
|
@@ -263,8 +198,6 @@ function warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPoli
|
|
|
263
198
|
]
|
|
264
199
|
: [];
|
|
265
200
|
const bashWarnings = bashPermissionPolicy.manager === 'allow' ? ['OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent allows bash while SDD subagents keep explicit permissions.'] : ['OpenCode top-level permission.bash is set to ask.'];
|
|
266
|
-
if (scope === 'project')
|
|
267
|
-
return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings, ...bashWarnings];
|
|
268
201
|
return [
|
|
269
202
|
'Restart OpenCode after installation so it reloads the user MCP config.',
|
|
270
203
|
'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
|
|
@@ -280,13 +213,6 @@ function createdConfigKeys(agentPlan) {
|
|
|
280
213
|
return [...keys, 'permission'];
|
|
281
214
|
}
|
|
282
215
|
function manualTestForScope(scope, databasePath, source) {
|
|
283
|
-
if (scope === 'project') {
|
|
284
|
-
return {
|
|
285
|
-
restart: 'Restart OpenCode after the config is installed.',
|
|
286
|
-
verify: 'Open the project in OpenCode and verify the vgxness MCP server is available.',
|
|
287
|
-
doctorCommand: createVgxnessMcpDoctorCommand(databasePath, source),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
216
|
return {
|
|
291
217
|
restart: 'Restart OpenCode after the user config is installed.',
|
|
292
218
|
verify: 'Open OpenCode and verify the vgxness MCP server is available from the user OpenCode config; project config may override it.',
|
|
@@ -15,7 +15,7 @@ export function createOpenCodeMcpVisibilityReport(options) {
|
|
|
15
15
|
const projectVisibleFromCurrentCwd = cwdInsideProject && projectConfigExists && projectHasVgxnessServer;
|
|
16
16
|
const userTarget = resolveOpenCodeMcpInstallTarget({ cwd: projectRoot, scope: 'user', env: options.env });
|
|
17
17
|
const userReport = userTarget.ok ? userVisibilityReport(userTarget.path, projectVisibleFromCurrentCwd) : undefined;
|
|
18
|
-
const selectedTargetPath =
|
|
18
|
+
const selectedTargetPath = userTarget.ok ? userTarget.path : '$HOME/.config/opencode/opencode.json';
|
|
19
19
|
const selectedReady = targetScope === 'user' ? userReport?.configExists === true && userReport.hasVgxnessServer : projectVisibleFromCurrentCwd;
|
|
20
20
|
return {
|
|
21
21
|
version: 1,
|
|
@@ -57,6 +57,9 @@ function buildGuidance(state) {
|
|
|
57
57
|
guidance.push('User scope targets `$HOME/.config/opencode/opencode.json` and this diagnostic reads it without writing provider config.');
|
|
58
58
|
guidance.push('Project OpenCode config can override user config for a workspace; inspect project config first when both define `mcp.vgxness`.');
|
|
59
59
|
}
|
|
60
|
+
if (state.targetScope === 'project') {
|
|
61
|
+
guidance.push('VGX-managed OpenCode installs are user-global only; project OpenCode config is read as external/manual diagnostics and will not be written by VGXNESS.');
|
|
62
|
+
}
|
|
60
63
|
if (state.userTargetMessage !== undefined)
|
|
61
64
|
guidance.push(state.userTargetMessage);
|
|
62
65
|
if (state.userOverridden)
|
|
@@ -65,8 +68,6 @@ function buildGuidance(state) {
|
|
|
65
68
|
guidance.push('The project-local `vgxness` server is not expected to appear from outside the worktree.');
|
|
66
69
|
guidance.push('Use `mcp install opencode --scope user --plan` to inspect the user config target without writing it.');
|
|
67
70
|
}
|
|
68
|
-
if (!state.configExists && state.targetScope === 'project')
|
|
69
|
-
guidance.push('Expected project-local config `.opencode/opencode.json` was not found.');
|
|
70
71
|
if (state.configExists && !state.hasVgxnessServer)
|
|
71
72
|
guidance.push('Project-local config exists, but it does not define `mcp.vgxness`.');
|
|
72
73
|
return guidance;
|