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.
Files changed (43) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/cli-flags.js +15 -2
  3. package/dist/cli/cli-help.js +13 -13
  4. package/dist/cli/commands/mcp-dispatcher.js +8 -4
  5. package/dist/cli/commands/setup-dispatcher.js +4 -4
  6. package/dist/cli/doctor-renderer.js +1 -1
  7. package/dist/cli/setup-plan-renderer.js +2 -2
  8. package/dist/cli/setup-status-renderer.js +2 -2
  9. package/dist/cli/tui/main-menu/main-menu-read-model.js +28 -28
  10. package/dist/cli/tui/main-menu/main-menu-render-shape.js +5 -1
  11. package/dist/cli/tui/opentui/main-menu/view.js +1 -1
  12. package/dist/cli/tui/setup/setup-tui-read-model.js +23 -23
  13. package/dist/cli/tui/setup/setup-tui-render-shape.js +15 -12
  14. package/dist/cli/tui/setup/setup-tui-state.js +1 -3
  15. package/dist/cli/tui/setup/setup-tui-view-helpers.js +18 -2
  16. package/dist/mcp/claude-code-config.js +2 -0
  17. package/dist/mcp/claude-code-scope.js +1 -0
  18. package/dist/mcp/claude-code-user-config.js +2 -0
  19. package/dist/mcp/client-install-claude-code-contract.js +15 -72
  20. package/dist/mcp/client-install-claude-code.js +4 -6
  21. package/dist/mcp/client-install-opencode-contract.js +5 -79
  22. package/dist/mcp/opencode-visibility.js +4 -3
  23. package/dist/mcp/provider-change-plan.js +17 -14
  24. package/dist/mcp/provider-doctor.js +38 -20
  25. package/dist/mcp/provider-health-types.js +19 -0
  26. package/dist/mcp/provider-status.js +29 -18
  27. package/dist/mcp/schema.js +5 -4
  28. package/dist/mcp/validation.js +11 -1
  29. package/dist/setup/backup-rollback-service.js +33 -2
  30. package/dist/setup/providers/claude-setup-adapter.js +13 -15
  31. package/dist/setup/providers/opencode-setup-adapter.js +12 -12
  32. package/dist/setup/setup-defaults.js +1 -0
  33. package/dist/setup/setup-plan.js +6 -6
  34. package/docs/architecture.md +3 -3
  35. package/docs/cli.md +29 -33
  36. package/docs/glossary.md +2 -2
  37. package/docs/mcp.md +1 -1
  38. package/docs/prd.md +3 -3
  39. package/docs/providers.md +14 -15
  40. package/docs/roadmap.md +1 -1
  41. package/docs/safety.md +1 -1
  42. package/docs/storage.md +1 -1
  43. 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
- 'Configure local storage and OpenCode integration with read-only previews first.',
16
- 'No provider config is written until the final confirmation.',
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
- 'OpenCode setup remains gated by final confirmation. Manual / none has no automatic install and no provider config will be written.',
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.writeAfterConfirm,
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
- 'Details are preview-only here; writes require final confirmation.',
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
- 'Read-only plan review',
61
- 'Plan summary ' + badgeLabels.readOnly,
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
- 'Review only: this screen does not write provider config.',
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
- 'No unconfirmed provider config write was performed.',
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, 'Setup was not completed.', 'No provider config was written.', 'No agent seeding was performed.', footer([setupFooterHints.close], width)];
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', 'No provider config write occurred.', footer([setupFooterHints.close], width)];
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:project', 'scope:user', 'install:mcp-plus-agents', 'install:mcp-only', 'overwrite:vgxness'],
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
- writeAfterConfirm: '[will write after confirm]',
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 = ['final confirmation', 'confirm and apply', 'not installable', 'read-only', 'requires confirmation', 'will write after confirm', 'error', 'cancelled'];
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, inspectClaudeCodeMcpConfig, resolveClaudeCodeMcpJsonPath } from './claude-code-config.js';
5
- import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
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
- if (resolvedScope.value.canonical !== 'project') {
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 : 'project';
121
- const cli = buildClaudeCodeMcpAddCommand({ scope: canonical });
122
- return { ...base(input, server, targets, preservedTopLevelKeys, false, overwriteVgxness, canonical, cli.ok ? cli.value : undefined, resolved.ok ? resolved.value.warnings : []), status: 'refused', reason, message };
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 = canonicalClaudeScope === 'project' ? resolveClaudeCodeMcpJsonPath(input.cwd) : canonicalClaudeScope === 'user' ? resolveClaudeCodeUserMcpJsonPath(input.env) : `claude-cli:${canonicalClaudeScope}:vgxness`;
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 ?? 'project',
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
- 'Project compatibility may write .mcp.json, .claude/agents/*.md, and a guarded project-root CLAUDE.md managed block after explicit confirmation/preflight.',
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 project config installation.' },
146
- { kind: 'manual-check', message: 'Open the project in Claude Code and verify the vgxness MCP server and project agents are visible.' },
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 project config write.', plan, server, [], []);
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 project config write.', plan, server, [], []);
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: plan.canonicalClaudeScope === 'user' ? 'write claude user provider config' : 'write claude project provider config',
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 supportedConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
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: true,
10
+ ok: false,
11
11
  scope,
12
- path: join(input.cwd, '.opencode', 'opencode.json'),
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
- const existingTargets = supportedConfigTargets
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 = targetScope === 'user' && userTarget.ok ? userTarget.path : projectTargetPath;
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;