vgxness 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/canonical-agent-projection.js +18 -0
- package/dist/agents/renderers/claude-renderer.js +3 -3
- package/dist/cli/cli-flags.js +1 -1
- package/dist/cli/cli-help.js +7 -7
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +2 -2
- package/dist/cli/commands/mcp-dispatcher.js +11 -1
- package/dist/cli/commands/setup-dispatcher.js +9 -0
- package/dist/cli/tui/main-menu/main-menu-read-model.js +41 -44
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +15 -15
- package/dist/cli/tui/opentui/main-menu/screen.js +39 -41
- package/dist/cli/tui/opentui/main-menu/smoke.js +1 -1
- package/dist/cli/tui/opentui/main-menu/view.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +15 -12
- package/dist/mcp/claude-code-agent-config.js +23 -7
- package/dist/mcp/claude-code-cli.js +71 -0
- package/dist/mcp/claude-code-config.js +1 -1
- package/dist/mcp/claude-code-project-memory.js +127 -0
- package/dist/mcp/claude-code-scope.js +18 -0
- package/dist/mcp/claude-code-user-config.js +55 -0
- package/dist/mcp/claude-code-user-memory.js +90 -0
- package/dist/mcp/client-install-claude-code-contract.js +91 -12
- package/dist/mcp/client-install-claude-code.js +133 -12
- package/dist/mcp/control-plane.js +18 -1
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/provider-change-plan.js +18 -6
- package/dist/mcp/provider-doctor.js +71 -5
- package/dist/mcp/provider-health-types.js +4 -0
- package/dist/mcp/provider-status.js +77 -8
- package/dist/mcp/schema.js +4 -3
- package/dist/sdd/schema.js +15 -0
- package/dist/sdd/sdd-workflow-service.js +59 -29
- package/dist/setup/providers/claude-setup-adapter.js +11 -7
- package/dist/setup/setup-plan.js +60 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +37 -2
- package/docs/glossary.md +2 -2
- package/docs/prd.md +2 -2
- package/docs/providers.md +33 -6
- package/docs/roadmap.md +1 -1
- package/package.json +1 -1
|
@@ -2,24 +2,30 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createManagedProviderConfigBackup } from '../setup/backup-rollback-service.js';
|
|
4
4
|
import { parseClaudeAgentFrontmatter } from './claude-code-agent-config.js';
|
|
5
|
+
import { runClaudeCodeCliCommand } from './claude-code-cli.js';
|
|
5
6
|
import { createClaudeCodeMcpServerConfig, inspectClaudeCodeMcpConfig, mergeClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
6
|
-
import {
|
|
7
|
+
import { inspectClaudeProjectMemory, mergeClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
8
|
+
import { expectedClaudeCodeRenderedAgents, expectedClaudeCodeRenderedUserAgents, planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
|
|
9
|
+
import { inspectClaudeCodeUserMcpConfig, mergeClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
|
|
10
|
+
import { inspectClaudeUserMemory, mergeClaudeUserMemory } from './claude-code-user-memory.js';
|
|
7
11
|
export async function installClaudeCodeMcpClient(input) {
|
|
8
12
|
const source = input.databasePathSource ?? 'flag';
|
|
9
13
|
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, [], []);
|
|
14
|
+
const plan = planClaudeCodeMcpInstall({ cwd: input.cwd, databasePath: input.databasePath, databasePathSource: source, ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}), ...(input.scope !== undefined ? { scope: input.scope } : {}), ...(input.env !== undefined ? { env: input.env } : {}) });
|
|
13
15
|
if (plan.status === 'refused')
|
|
14
16
|
return refusal(plan.reason, plan.message, plan, server, [], []);
|
|
17
|
+
if (!input.confirmed)
|
|
18
|
+
return refusal('confirmation_required', '`mcp install claude` requires explicit --yes before any project config write.', plan, server, [], []);
|
|
15
19
|
if (input.preflight === undefined) {
|
|
16
20
|
return refusal('preflight_failed', 'Claude Code provider config writes require VGXNESS preflight before any project config write.', plan, server, [], []);
|
|
17
21
|
}
|
|
18
|
-
const preflightPaths = unique(plan.targets.
|
|
22
|
+
const preflightPaths = unique(plan.targets.flatMap((target) => (isMutatingTarget(target) ? [target.path] : [])));
|
|
23
|
+
if (plan.canonicalClaudeScope !== 'project')
|
|
24
|
+
preflightPaths.unshift(plan.targetPath);
|
|
19
25
|
for (const targetPath of preflightPaths) {
|
|
20
26
|
const preflight = await input.preflight({
|
|
21
27
|
category: 'provider-tool',
|
|
22
|
-
operation: 'write claude project provider config',
|
|
28
|
+
operation: plan.canonicalClaudeScope === 'user' ? 'write claude user provider config' : 'write claude project provider config',
|
|
23
29
|
targetPath,
|
|
24
30
|
workspaceRoot: input.cwd,
|
|
25
31
|
providerToolName: 'claude-code',
|
|
@@ -32,9 +38,22 @@ export async function installClaudeCodeMcpClient(input) {
|
|
|
32
38
|
}
|
|
33
39
|
const backups = [];
|
|
34
40
|
const writtenPaths = [];
|
|
35
|
-
|
|
41
|
+
if (plan.canonicalClaudeScope === 'local') {
|
|
42
|
+
if (input.cliRunner === undefined)
|
|
43
|
+
return refusal('preflight_failed', 'Claude CLI MCP registration apply requires an injected runner boundary; read-only plans never execute Claude Code.', plan, server, [], []);
|
|
44
|
+
if (plan.cliCommand === undefined)
|
|
45
|
+
return refusal('preflight_failed', 'Claude CLI MCP registration command is unavailable.', plan, server, [], []);
|
|
46
|
+
const cli = await runClaudeCodeCliCommand(plan.cliCommand, input.cliRunner);
|
|
47
|
+
const cliResult = { exitCode: cli.exitCode, stdout: cli.stdout, stderr: cli.stderr, preview: cli.command.preview };
|
|
48
|
+
if (cli.exitCode !== 0)
|
|
49
|
+
return refusal('preflight_failed', `Claude CLI MCP registration failed with exit code ${cli.exitCode ?? 'unknown'}.`, plan, server, [], [], cliResult);
|
|
50
|
+
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, cliResult };
|
|
51
|
+
}
|
|
52
|
+
if (plan.canonicalClaudeScope === 'user')
|
|
53
|
+
return applyUserInstall(input, plan, server, writtenPaths, backups);
|
|
54
|
+
const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block'));
|
|
36
55
|
for (const target of existingTargets) {
|
|
37
|
-
const backup = createBackup(target.path);
|
|
56
|
+
const backup = createBackup(target.path, target.kind === 'project-memory' ? 'project-memory' : 'config');
|
|
38
57
|
if (!backup.ok)
|
|
39
58
|
return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
|
|
40
59
|
backups.push(toBackupSummary(backup.value));
|
|
@@ -54,6 +73,11 @@ export async function installClaudeCodeMcpClient(input) {
|
|
|
54
73
|
return refusal('post_write_validation_failed', `Claude agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
|
|
55
74
|
writtenPaths.push(agent.path);
|
|
56
75
|
}
|
|
76
|
+
const projectMemoryWrite = writeProjectMemory(plan);
|
|
77
|
+
if (!projectMemoryWrite.ok)
|
|
78
|
+
return refusal('post_write_validation_failed', projectMemoryWrite.error.message, plan, server, writtenPaths, backups);
|
|
79
|
+
if (projectMemoryWrite.value !== undefined)
|
|
80
|
+
writtenPaths.push(projectMemoryWrite.value);
|
|
57
81
|
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
82
|
}
|
|
59
83
|
function writeMcpJson(cwd, plan, server) {
|
|
@@ -68,11 +92,101 @@ function writeMcpJson(cwd, plan, server) {
|
|
|
68
92
|
const after = inspectClaudeCodeMcpConfig(cwd);
|
|
69
93
|
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
94
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
95
|
+
function writeUserMcpJson(env, plan, server) {
|
|
96
|
+
const target = plan.targets.find((item) => item.kind === 'user-mcp-json');
|
|
97
|
+
if (target === undefined)
|
|
98
|
+
return { ok: true, value: plan.targetPath };
|
|
99
|
+
if (target.action === 'blocked')
|
|
100
|
+
return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude user config is blocked.' } };
|
|
101
|
+
const state = inspectClaudeCodeUserMcpConfig(env);
|
|
102
|
+
if (state.status === 'invalid' || state.status === 'conflicting')
|
|
103
|
+
return { ok: false, error: { code: 'validation_failed', message: state.message } };
|
|
104
|
+
if (target.action === 'create' && existsSync(target.path))
|
|
105
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude user ~/.claude.json appeared after planning; rerun apply to merge safely.' } };
|
|
106
|
+
if (state.path !== target.path)
|
|
107
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude user config path changed after planning; rerun apply.' } };
|
|
108
|
+
const merged = mergeClaudeCodeUserMcpConfig(state.parsed ? state.config : {}, server);
|
|
109
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
110
|
+
writeFileSync(target.path, `${JSON.stringify(merged, null, 2)}\n`);
|
|
111
|
+
const after = inspectClaudeCodeUserMcpConfig(env);
|
|
112
|
+
return after.status === 'configured' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude user ~/.claude.json did not validate after write.' } };
|
|
113
|
+
}
|
|
114
|
+
function writeProjectMemory(plan) {
|
|
115
|
+
const target = plan.targets.find((item) => item.kind === 'project-memory');
|
|
116
|
+
if (target === undefined || target.action === 'none')
|
|
117
|
+
return { ok: true, value: undefined };
|
|
118
|
+
if (target.action === 'blocked')
|
|
119
|
+
return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude project memory is blocked.' } };
|
|
120
|
+
const state = inspectClaudeProjectMemory(dirname(target.path));
|
|
121
|
+
if (state.action !== target.action)
|
|
122
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude project memory changed after planning; rerun apply.' } };
|
|
123
|
+
const merged = mergeClaudeProjectMemory(state);
|
|
124
|
+
if (!merged.ok)
|
|
125
|
+
return merged;
|
|
126
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
127
|
+
writeFileSync(target.path, merged.value.contents);
|
|
128
|
+
const after = inspectClaudeProjectMemory(dirname(target.path));
|
|
129
|
+
return after.status === 'managed-current' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude project memory did not validate as managed-current after write.' } };
|
|
130
|
+
}
|
|
131
|
+
async function applyUserInstall(input, plan, server, writtenPaths, backups) {
|
|
132
|
+
const existingTargets = plan.targets.filter((target) => target.kind !== 'cli-mcp-registration' && (target.action === 'merge' || target.action === 'update-vgxness' || target.action === 'append-managed-block' || target.action === 'update-managed-block'));
|
|
133
|
+
for (const target of existingTargets) {
|
|
134
|
+
const backup = createBackup(target.path, target.kind === 'user-memory' ? 'user-memory' : 'config', 'user');
|
|
135
|
+
if (!backup.ok)
|
|
136
|
+
return refusal('backup_failed', backup.error.message, plan, server, writtenPaths, backups);
|
|
137
|
+
backups.push(toBackupSummary(backup.value));
|
|
138
|
+
}
|
|
139
|
+
const mcpWrite = writeUserMcpJson(input.env, plan, server);
|
|
140
|
+
if (!mcpWrite.ok)
|
|
141
|
+
return refusal('post_write_validation_failed', mcpWrite.error.message, plan, server, writtenPaths, backups);
|
|
142
|
+
writtenPaths.push(mcpWrite.value);
|
|
143
|
+
for (const agent of expectedClaudeCodeRenderedUserAgents(input.cwd, input.env)) {
|
|
144
|
+
const target = plan.targets.find((item) => item.kind === 'agent-file' && item.path === agent.path);
|
|
145
|
+
if (target?.action !== 'create' && target?.action !== 'update-vgxness')
|
|
146
|
+
continue;
|
|
147
|
+
mkdirSync(dirname(agent.path), { recursive: true });
|
|
148
|
+
writeFileSync(agent.path, agent.contents);
|
|
149
|
+
const validation = parseClaudeAgentFrontmatter(readFileSync(agent.path, 'utf8'));
|
|
150
|
+
if (!validation.ok || validation.data.name !== agent.agentName)
|
|
151
|
+
return refusal('post_write_validation_failed', `Claude user agent ${agent.agentName} failed post-write validation.`, plan, server, writtenPaths, backups);
|
|
152
|
+
writtenPaths.push(agent.path);
|
|
153
|
+
}
|
|
154
|
+
const memoryWrite = writeUserMemory(input.env, plan);
|
|
155
|
+
if (!memoryWrite.ok)
|
|
156
|
+
return refusal('post_write_validation_failed', memoryWrite.error.message, plan, server, writtenPaths, backups);
|
|
157
|
+
if (memoryWrite.value !== undefined)
|
|
158
|
+
writtenPaths.push(memoryWrite.value);
|
|
159
|
+
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 };
|
|
160
|
+
}
|
|
161
|
+
function writeUserMemory(env, plan) {
|
|
162
|
+
const target = plan.targets.find((item) => item.kind === 'user-memory');
|
|
163
|
+
if (target === undefined || target.action === 'none')
|
|
164
|
+
return { ok: true, value: undefined };
|
|
165
|
+
if (target.action === 'blocked')
|
|
166
|
+
return { ok: false, error: { code: 'validation_failed', message: target.reason ?? 'Claude user memory is blocked.' } };
|
|
167
|
+
const state = inspectClaudeUserMemory(env);
|
|
168
|
+
if (state.path !== target.path || state.action !== target.action)
|
|
169
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Claude user memory changed after planning; rerun apply.' } };
|
|
170
|
+
const merged = mergeClaudeUserMemory(state);
|
|
171
|
+
if (!merged.ok)
|
|
172
|
+
return merged;
|
|
173
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
174
|
+
writeFileSync(target.path, merged.value.contents);
|
|
175
|
+
const after = inspectClaudeUserMemory(env);
|
|
176
|
+
return after.status === 'managed-current' ? { ok: true, value: target.path } : { ok: false, error: { code: 'validation_failed', message: 'Claude user memory did not validate as managed-current after write.' } };
|
|
177
|
+
}
|
|
178
|
+
function createBackup(path, kind, scope = 'project') {
|
|
179
|
+
return createManagedProviderConfigBackup({
|
|
180
|
+
targetPath: path,
|
|
181
|
+
provider: 'claude',
|
|
182
|
+
scope,
|
|
183
|
+
createdByOperation: 'mcp-client-install-claude-code',
|
|
184
|
+
reason: kind === 'project-memory' ? 'pre-project-memory-update-safety' : kind === 'user-memory' ? 'pre-user-memory-update-safety' : 'pre-merge-safety',
|
|
185
|
+
description: kind === 'project-memory' ? 'Backup existing Claude Code project memory before appending or updating the VGXNESS managed block.' : kind === 'user-memory' ? 'Backup existing Claude Code user memory before appending or updating the VGXNESS managed block.' : 'Backup existing Claude Code config before merging VGXNESS MCP or agent configuration.',
|
|
186
|
+
});
|
|
73
187
|
}
|
|
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 };
|
|
188
|
+
function refusal(reason, message, plan, server, writtenPaths, backups, cliResult) {
|
|
189
|
+
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, ...(cliResult === undefined ? {} : { cliResult }) };
|
|
76
190
|
}
|
|
77
191
|
function applySafety(plan) {
|
|
78
192
|
return { ...plan.safety, operation: 'apply', mutating: true, writesProviderConfig: true };
|
|
@@ -83,3 +197,10 @@ function toBackupSummary(backup) {
|
|
|
83
197
|
function unique(values) {
|
|
84
198
|
return [...new Set(values)];
|
|
85
199
|
}
|
|
200
|
+
function isMutatingTarget(target) {
|
|
201
|
+
if (target.kind === 'cli-mcp-registration')
|
|
202
|
+
return false;
|
|
203
|
+
if (target.action === 'blocked' || target.action === 'none')
|
|
204
|
+
return false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
@@ -29,7 +29,7 @@ export function callVgxTool(call, services) {
|
|
|
29
29
|
case 'vgxness_sdd_get_readiness':
|
|
30
30
|
return auditedEnvelope(validated.tool, services.sdd.getReady(validated.input), services, sddReadinessAuditPayload(validated.input));
|
|
31
31
|
case 'vgxness_sdd_save_artifact':
|
|
32
|
-
return
|
|
32
|
+
return auditedEnvelope(validated.tool, services.sdd.saveArtifact(validated.input), services, sddSaveAuditPayload(validated.input));
|
|
33
33
|
case 'vgxness_sdd_accept_artifact':
|
|
34
34
|
return auditedEnvelope(validated.tool, services.sdd.acceptArtifact(toAcceptArtifactServiceInput(validated.input)), services, sddAcceptanceAuditPayload(validated.input));
|
|
35
35
|
case 'vgxness_sdd_get_artifact':
|
|
@@ -170,6 +170,23 @@ function sddAcceptanceAuditPayload(input) {
|
|
|
170
170
|
}),
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
function sddSaveAuditPayload(input) {
|
|
174
|
+
return {
|
|
175
|
+
runId: input.runId,
|
|
176
|
+
title: 'Audit: sdd-artifact-saved',
|
|
177
|
+
relatedType: 'sdd-artifact',
|
|
178
|
+
relatedId: `${input.change}:${input.phase}`,
|
|
179
|
+
payload: (value) => ({
|
|
180
|
+
eventType: 'sdd-artifact-saved',
|
|
181
|
+
project: input.project,
|
|
182
|
+
change: input.change,
|
|
183
|
+
phase: input.phase,
|
|
184
|
+
topicKey: value.topicKey,
|
|
185
|
+
artifactId: value.id,
|
|
186
|
+
...(input.agentId === undefined ? {} : { agentId: input.agentId }),
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
173
190
|
function governanceReportAuditPayload(input) {
|
|
174
191
|
return {
|
|
175
192
|
runId: input.runId,
|
package/dist/mcp/index.js
CHANGED
|
@@ -6,7 +6,12 @@ export * from './client-install-claude-code.js';
|
|
|
6
6
|
export * from './client-install-claude-code-contract.js';
|
|
7
7
|
export * from './client-setup-preview.js';
|
|
8
8
|
export * from './claude-code-agent-config.js';
|
|
9
|
+
export * from './claude-code-cli.js';
|
|
9
10
|
export * from './claude-code-config.js';
|
|
11
|
+
export * from './claude-code-project-memory.js';
|
|
12
|
+
export * from './claude-code-scope.js';
|
|
13
|
+
export * from './claude-code-user-config.js';
|
|
14
|
+
export * from './claude-code-user-memory.js';
|
|
10
15
|
export * from './control-plane.js';
|
|
11
16
|
export * from './doctor.js';
|
|
12
17
|
export * from './opencode-visibility.js';
|
|
@@ -3,6 +3,7 @@ import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.
|
|
|
3
3
|
import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
|
|
4
4
|
import { ProviderDoctorService } from './provider-doctor.js';
|
|
5
5
|
import { ProviderStatusService } from './provider-status.js';
|
|
6
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
6
7
|
export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
7
8
|
export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
8
9
|
const safety = {
|
|
@@ -75,10 +76,13 @@ export class ProviderChangePlanService {
|
|
|
75
76
|
const doctor = (this.deps.providerDoctor ?? new ProviderDoctorService()).getDoctor({ project: normalized.project, scope: normalized.scope, providerAdapter: 'claude', workspaceRoot: normalized.workspaceRoot, env: this.deps.env ?? process.env, payloadMode: normalized.payloadMode });
|
|
76
77
|
if (!doctor.ok)
|
|
77
78
|
return doctor;
|
|
79
|
+
const claudeScope = resolveClaudeCodeScope(normalized.scope);
|
|
80
|
+
if (!claudeScope.ok)
|
|
81
|
+
return claudeScope;
|
|
78
82
|
const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
|
|
79
83
|
if (!databasePath.ok)
|
|
80
84
|
return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
|
|
81
|
-
const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source });
|
|
85
|
+
const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope: normalized.scope, env: this.deps.env ?? process.env });
|
|
82
86
|
return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -180,7 +184,7 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
|
|
|
180
184
|
supported: true,
|
|
181
185
|
status: installPlan.status === 'refused' ? 'blocked' : 'planned',
|
|
182
186
|
...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
|
|
183
|
-
summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update project .mcp.json and
|
|
187
|
+
summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update ${installPlan.canonicalClaudeScope === 'user' ? '~/.claude.json, user agents, and ~/.claude/CLAUDE.md' : 'project .mcp.json, agent files, and project-root CLAUDE.md'} as needed.`,
|
|
184
188
|
providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
|
|
185
189
|
statusSummary: statusSummary(status),
|
|
186
190
|
statusFindings: status.config,
|
|
@@ -262,10 +266,18 @@ function previewEffects(plan) {
|
|
|
262
266
|
};
|
|
263
267
|
}
|
|
264
268
|
function previewEffectsClaude(plan) {
|
|
269
|
+
const projectMemory = projectMemoryPreview(plan.targets);
|
|
265
270
|
if (plan.status === 'refused')
|
|
266
|
-
return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message };
|
|
267
|
-
const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json');
|
|
268
|
-
return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install' };
|
|
271
|
+
return { action: 'refused', targetPath: plan.targetPath, backupRequired: false, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'refused', refusalReason: plan.reason, refusalMessage: plan.message, ...(projectMemory === undefined ? {} : { projectMemory }) };
|
|
272
|
+
const mcpTarget = plan.targets.find((target) => target.kind === 'mcp-json' || target.kind === 'user-mcp-json');
|
|
273
|
+
return { action: mcpTarget?.action === 'create' ? 'would-create' : 'would-merge', targetPath: plan.targetPath, backupRequired: plan.backupRequired, preservedTopLevelKeys: plan.preservedTopLevelKeys, serverCommand: ['vgxness', ...plan.server.args], installsAgents: true, agentNames: plan.agentNames, installPlanStatus: 'would_install', ...(projectMemory === undefined ? {} : { projectMemory }) };
|
|
274
|
+
}
|
|
275
|
+
function projectMemoryPreview(targets) {
|
|
276
|
+
const target = targets.find((item) => item.kind === 'project-memory' || item.kind === 'user-memory');
|
|
277
|
+
if (target === undefined)
|
|
278
|
+
return undefined;
|
|
279
|
+
const action = target.action === 'create' ? 'would-create' : target.action === 'append-managed-block' ? 'would-append' : target.action === 'update-managed-block' ? 'would-update-managed-block' : target.action === 'none' ? 'up-to-date' : 'refused';
|
|
280
|
+
return { path: target.path, status: target.status, action, backupRequired: target.backupRequired, ...(target.reason === undefined ? {} : { message: target.reason }) };
|
|
269
281
|
}
|
|
270
282
|
function descriptiveBackupPolicy(backupRequired) {
|
|
271
283
|
return {
|
|
@@ -300,7 +312,7 @@ function risksForOpenCode(status, doctor, plan, source) {
|
|
|
300
312
|
return risks;
|
|
301
313
|
}
|
|
302
314
|
function risksForClaude(status, doctor, plan, source) {
|
|
303
|
-
const risks = ['Claude project .mcp.json may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
|
|
315
|
+
const risks = [plan.canonicalClaudeScope === 'user' ? 'Claude user/global files affect this OS user; backups are required before merging existing files.' : 'Claude project .mcp.json and project-root CLAUDE.md may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
|
|
304
316
|
if (status.status !== 'ready')
|
|
305
317
|
risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
|
|
306
318
|
if (doctor.status !== 'healthy')
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
|
|
3
3
|
import { claudeAdvisoryPaths, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
4
|
+
import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
5
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
6
|
+
import { inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
|
|
7
|
+
import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
4
8
|
import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
|
|
5
9
|
import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
|
|
6
|
-
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, providerHealthFailure, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
|
|
10
|
+
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
|
|
7
11
|
import { inspectOpenCodeConfigPaths } from './provider-status.js';
|
|
8
12
|
export class ProviderDoctorService {
|
|
9
13
|
deps;
|
|
@@ -83,24 +87,42 @@ export class ProviderDoctorService {
|
|
|
83
87
|
};
|
|
84
88
|
}
|
|
85
89
|
getClaudeDoctor(normalized) {
|
|
90
|
+
const resolvedScope = resolveClaudeCodeScope(normalized.scope);
|
|
91
|
+
if (!resolvedScope.ok)
|
|
92
|
+
return resolvedScope;
|
|
93
|
+
if (isUserGlobalScope(normalized.scope) || resolvedScope.value.canonical === 'local')
|
|
94
|
+
return this.getClaudeUserGlobalDoctor(normalized, resolvedScope.value.canonical, resolvedScope.value.warnings);
|
|
86
95
|
const mcp = inspectClaudeCodeMcpConfig(normalized.workspaceRoot);
|
|
87
96
|
const agents = inspectClaudeCodeAgents(normalized.workspaceRoot);
|
|
97
|
+
const projectMemory = inspectClaudeProjectMemory(normalized.workspaceRoot);
|
|
88
98
|
const advisoryPaths = claudeAdvisoryPaths(normalized.workspaceRoot);
|
|
89
|
-
const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), ...advisoryPaths];
|
|
99
|
+
const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...advisoryPaths];
|
|
90
100
|
const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
|
|
91
101
|
const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
|
|
92
|
-
const
|
|
102
|
+
const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
|
|
103
|
+
const badFrontmatter = agents.agents.filter((agent) => agent.exists && agent.frontmatter === 'invalid');
|
|
93
104
|
const badMarkers = agents.agents.filter((agent) => agent.exists && !agent.generatedMarker);
|
|
94
105
|
const memoryFiles = advisoryPaths.filter((path) => existsSync(path));
|
|
95
106
|
const checks = [
|
|
96
107
|
{ id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
|
|
97
|
-
{ id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected for project
|
|
108
|
+
{ id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected for CLI MCP registration diagnostics, project .mcp.json compatibility, and Claude agents support.' },
|
|
109
|
+
{ id: 'claude-scope', status: 'pass', detail: 'Claude scope project resolved to canonical project.' },
|
|
110
|
+
{ id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; CLI availability is diagnosed during explicit apply/preflight only.' },
|
|
98
111
|
{ id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed .mcp.json before installing VGXNESS Claude support.' } : {}) },
|
|
99
112
|
{ id: 'claude-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness before applying VGXNESS Claude support.' } : {}) },
|
|
100
113
|
{ id: 'claude-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude project agents directory exists.' : 'Claude project agents directory is missing; confirmed apply may create it.' },
|
|
101
|
-
{
|
|
114
|
+
{
|
|
115
|
+
id: 'claude-vgxness-agents',
|
|
116
|
+
status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass',
|
|
117
|
+
detail: blockingAgents.length > 0
|
|
118
|
+
? `Conflicting or invalid VGXNESS Claude agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.`
|
|
119
|
+
: missingAgents.length > 0
|
|
120
|
+
? `Missing VGXNESS Claude agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.`
|
|
121
|
+
: 'Expected VGXNESS Claude agent targets were inspected.',
|
|
122
|
+
},
|
|
102
123
|
{ id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
|
|
103
124
|
{ id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
|
|
125
|
+
claudeProjectMemoryCheck(projectMemory),
|
|
104
126
|
{ id: 'claude-project-memory-advisory', status: memoryFiles.length === 0 ? 'pass' : 'warn', detail: memoryFiles.length === 0 ? 'No Claude project memory/settings advisory files were found.' : `Advisory only; VGXNESS will not modify: ${memoryFiles.join(', ')}.` },
|
|
105
127
|
readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
|
|
106
128
|
];
|
|
@@ -111,6 +133,37 @@ export class ProviderDoctorService {
|
|
|
111
133
|
const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
|
|
112
134
|
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
|
|
113
135
|
}
|
|
136
|
+
getClaudeUserGlobalDoctor(normalized, canonicalScope = 'user', scopeWarnings = []) {
|
|
137
|
+
const mcp = inspectClaudeCodeUserMcpConfig(normalized.env);
|
|
138
|
+
const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
|
|
139
|
+
const userMemory = inspectClaudeUserMemory(normalized.env);
|
|
140
|
+
const checkedPathList = [mcp.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path];
|
|
141
|
+
const before = snapshotPaths(checkedPathList, normalized.workspaceRoot);
|
|
142
|
+
const missingAgents = agents.agents.filter((agent) => agent.status === 'missing');
|
|
143
|
+
const blockingAgents = agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid');
|
|
144
|
+
const badFrontmatter = agents.agents.filter((agent) => agent.exists && agent.frontmatter === 'invalid');
|
|
145
|
+
const badMarkers = agents.agents.filter((agent) => agent.exists && !agent.generatedMarker);
|
|
146
|
+
const checks = [
|
|
147
|
+
{ id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
|
|
148
|
+
{ id: 'provider-supported', status: 'pass', detail: `Claude ${canonicalScope} scope supports guarded ~/.claude.json MCP merge, user agents, and ~/.claude/CLAUDE.md managed block after confirmation/preflight.` },
|
|
149
|
+
{ id: 'claude-scope', status: scopeWarnings.length === 0 ? 'pass' : 'warn', detail: scopeWarnings.join(' ') || `Claude scope resolved to canonical ${canonicalScope}.` },
|
|
150
|
+
{ id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; no provider process was launched.' },
|
|
151
|
+
{ id: 'claude-user-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed ~/.claude.json before installing VGXNESS Claude user support.' } : {}) },
|
|
152
|
+
{ id: 'claude-user-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness in ~/.claude.json before applying VGXNESS Claude user support.' } : {}) },
|
|
153
|
+
{ id: 'claude-user-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude user agents directory exists.' : 'Claude user agents directory is missing; confirmed apply may create it.' },
|
|
154
|
+
{ id: 'claude-user-vgxness-agents', status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass', detail: blockingAgents.length > 0 ? `Conflicting or invalid VGXNESS Claude user agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.` : missingAgents.length > 0 ? `Missing VGXNESS Claude user agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.` : 'Expected VGXNESS Claude user agent targets were inspected.' },
|
|
155
|
+
{ id: 'claude-user-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude user agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
|
|
156
|
+
{ id: 'claude-user-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude user agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
|
|
157
|
+
{ ...claudeProjectMemoryCheck(userMemory), id: 'claude-user-memory-managed-block' },
|
|
158
|
+
readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
|
|
159
|
+
];
|
|
160
|
+
const status = rollupProviderDoctor(checks.map((check) => check.status));
|
|
161
|
+
const compactChecksValue = compactChecks(checks, normalized.payloadMode);
|
|
162
|
+
const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
|
|
163
|
+
const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
|
|
164
|
+
const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
|
|
165
|
+
return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
|
|
166
|
+
}
|
|
114
167
|
}
|
|
115
168
|
function summarizeDoctor(status, failedCount, recommendationCount) {
|
|
116
169
|
if (status === 'healthy')
|
|
@@ -223,6 +276,19 @@ function readonlySafetyCheck(before, after) {
|
|
|
223
276
|
}
|
|
224
277
|
: { id: 'provider-config-readonly-safety', status: 'fail', detail: 'Provider config path existence or mtimes changed during read-only doctor.' };
|
|
225
278
|
}
|
|
279
|
+
function claudeProjectMemoryCheck(state) {
|
|
280
|
+
const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
|
|
281
|
+
return {
|
|
282
|
+
id: 'claude-project-memory-managed-block',
|
|
283
|
+
status,
|
|
284
|
+
detail: state.message,
|
|
285
|
+
...(state.status === 'blocked'
|
|
286
|
+
? { remediation: 'Manually reconcile VGXNESS Claude project-memory markers before installing.' }
|
|
287
|
+
: state.status === 'managed-current'
|
|
288
|
+
? {}
|
|
289
|
+
: { remediation: 'Run confirmed Claude project install when ready.' }),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
226
292
|
function snapshotPaths(paths, workspaceRoot) {
|
|
227
293
|
const unique = [...paths, `${workspaceRoot}/.vgx`];
|
|
228
294
|
return Object.fromEntries(unique.map((path) => [path, existsSync(path) ? statSync(path).mtimeMs : false]));
|
|
@@ -11,6 +11,10 @@ export const PROVIDER_HEALTH_SAFETY = {
|
|
|
11
11
|
runsRepair: false,
|
|
12
12
|
executesProvider: false,
|
|
13
13
|
};
|
|
14
|
+
export const CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES = ['status', 'doctor', 'change-plan'];
|
|
15
|
+
export function isUserGlobalScope(scope) {
|
|
16
|
+
return scope === 'personal' || scope === 'user';
|
|
17
|
+
}
|
|
14
18
|
export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor', 'vgxness_provider_change_plan'];
|
|
15
19
|
export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
|
|
16
20
|
'vgxness_provider_status',
|
|
@@ -2,10 +2,14 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { inspectClaudeCodeAgents } from './claude-code-agent-config.js';
|
|
4
4
|
import { claudeAdvisoryPaths, claudeMcpConfigPathStatus, claudeMcpEntryStatus, inspectClaudeCodeMcpConfig } from './claude-code-config.js';
|
|
5
|
+
import { inspectClaudeProjectMemory } from './claude-code-project-memory.js';
|
|
6
|
+
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
7
|
+
import { claudeUserMcpConfigPathStatus, claudeUserMcpEntryStatus, inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
|
|
8
|
+
import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
|
|
5
9
|
import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
|
|
6
10
|
import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
|
|
7
11
|
import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
|
|
8
|
-
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, providerHealthFailure, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
|
|
12
|
+
import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
|
|
9
13
|
const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
|
|
10
14
|
export class ProviderStatusService {
|
|
11
15
|
deps;
|
|
@@ -87,24 +91,30 @@ export class ProviderStatusService {
|
|
|
87
91
|
return { change, ...(status.ok ? { status: status.value } : {}), ...(next.ok ? { next: next.value } : {}) };
|
|
88
92
|
}
|
|
89
93
|
getClaudeStatus(normalized) {
|
|
94
|
+
const resolvedScope = resolveClaudeCodeScope(normalized.scope);
|
|
95
|
+
if (!resolvedScope.ok)
|
|
96
|
+
return resolvedScope;
|
|
97
|
+
if (isUserGlobalScope(normalized.scope) || resolvedScope.value.canonical === 'local')
|
|
98
|
+
return this.getClaudeUserGlobalStatus(normalized, resolvedScope.value.canonical, resolvedScope.value.warnings);
|
|
90
99
|
const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
|
|
91
100
|
const mcpState = inspectClaudeCodeMcpConfig(normalized.workspaceRoot);
|
|
92
101
|
const agents = inspectClaudeCodeAgents(normalized.workspaceRoot);
|
|
93
|
-
const
|
|
102
|
+
const projectMemory = inspectClaudeProjectMemory(normalized.workspaceRoot);
|
|
103
|
+
const paths = [claudeMcpConfigPathStatus(mcpState), claudeProjectMemoryPathStatus(projectMemory)];
|
|
94
104
|
const mcpEntry = claudeMcpEntryStatus(mcpState);
|
|
95
105
|
const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
|
|
96
106
|
const advisory = claudeAdvisoryPaths(normalized.workspaceRoot).filter((path) => existsSync(path));
|
|
97
|
-
const configStatus = claudeConfigHealthStatus([paths
|
|
107
|
+
const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
|
|
98
108
|
const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
|
|
99
109
|
const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
|
|
100
|
-
const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), ...claudeAdvisoryPaths(normalized.workspaceRoot)] : [mcpState.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path), ...advisory];
|
|
101
|
-
const tools = requiredToolPresence();
|
|
102
|
-
const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, advisory, sdd, mcpRequiredTools: tools };
|
|
103
|
-
const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
|
|
110
|
+
const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), projectMemory.path, ...claudeAdvisoryPaths(normalized.workspaceRoot)] : [mcpState.path, projectMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path), ...advisory];
|
|
111
|
+
const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; CLI presence is checked during explicit apply/preflight only.' }];
|
|
112
|
+
const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, projectMemory, advisory, sdd, mcpRequiredTools: tools };
|
|
113
|
+
const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), projectMemory: { status: projectMemory.status, action: projectMemory.action }, advisory, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
|
|
104
114
|
const originalBytes = Buffer.byteLength(JSON.stringify(verboseShape), 'utf8');
|
|
105
115
|
const compactBytes = Buffer.byteLength(JSON.stringify(compactShape), 'utf8');
|
|
106
116
|
const issueCount = [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length;
|
|
107
|
-
const warningCount = advisory.length;
|
|
117
|
+
const warningCount = advisory.length + resolvedScope.value.warnings.length + paths.filter((path) => path.status === 'warn').length;
|
|
108
118
|
return {
|
|
109
119
|
ok: true,
|
|
110
120
|
value: {
|
|
@@ -136,6 +146,53 @@ export class ProviderStatusService {
|
|
|
136
146
|
},
|
|
137
147
|
};
|
|
138
148
|
}
|
|
149
|
+
getClaudeUserGlobalStatus(normalized, canonicalScope = 'user', scopeWarnings = []) {
|
|
150
|
+
const canonicalAgentManifest = (this.deps.canonicalAgentManifestDiagnostic ?? buildCanonicalAgentManifestDiagnostic)();
|
|
151
|
+
const mcpState = inspectClaudeCodeUserMcpConfig(normalized.env);
|
|
152
|
+
const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
|
|
153
|
+
const userMemory = inspectClaudeUserMemory(normalized.env);
|
|
154
|
+
const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory), label: 'user ~/.claude/CLAUDE.md managed block' };
|
|
155
|
+
const paths = [claudeUserMcpConfigPathStatus(mcpState), userMemoryPathStatus];
|
|
156
|
+
const mcpEntry = claudeUserMcpEntryStatus(mcpState);
|
|
157
|
+
const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
|
|
158
|
+
const configStatus = claudeConfigHealthStatus([...paths.map((path) => path.status), ...agentStatuses]);
|
|
159
|
+
const tools = [...requiredToolPresence(), { tool: 'claude-cli', present: false, diagnostic: 'Read-only status does not execute `claude --version`; no Claude Code process was launched.' }];
|
|
160
|
+
const status = rollupProviderHealth([canonicalAgentManifest.status, configStatus]);
|
|
161
|
+
const sdd = normalized.change.length > 0 ? this.readSdd(normalized.project, normalized.change) : undefined;
|
|
162
|
+
const checkedPaths = normalized.payloadMode === 'verbose' ? [mcpState.path, agents.directoryPath, ...agents.agents.map((agent) => agent.path), userMemory.path] : [mcpState.path, userMemory.path, ...agents.agents.filter((agent) => agent.exists || agent.status !== 'missing').map((agent) => agent.path)];
|
|
163
|
+
const verboseShape = { config: { status: configStatus, paths, mcpEntry }, canonicalAgentManifest, agents, userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, scopeWarnings, sdd, mcpRequiredTools: tools };
|
|
164
|
+
const compactShape = { config: { status: configStatus, paths: compactPaths(paths, 'compact'), mcpEntry: compactMcpEntry(mcpEntry, 'compact') }, canonicalAgentManifest, agentSummary: summarizeClaudeAgents(agents), userMemory: { status: userMemory.status, action: userMemory.action }, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, sdd: sdd === undefined ? undefined : compactSdd(sdd, 'compact'), mcpRequiredTools: tools };
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
value: {
|
|
168
|
+
version: 1,
|
|
169
|
+
kind: 'provider-status',
|
|
170
|
+
project: normalized.project,
|
|
171
|
+
providerAdapter: 'claude',
|
|
172
|
+
scope: normalized.scope,
|
|
173
|
+
workspaceRoot: normalized.workspaceRoot,
|
|
174
|
+
status,
|
|
175
|
+
payloadMode: normalized.payloadMode,
|
|
176
|
+
overallStatus: status,
|
|
177
|
+
inspectedPaths: checkedPaths,
|
|
178
|
+
issueCount: [canonicalAgentManifest.status, configStatus, ...agentStatuses].filter((item) => item === 'fail' || item === 'not-configured').length,
|
|
179
|
+
warningCount: scopeWarnings.length + paths.filter((path) => path.status === 'warn').length + 1,
|
|
180
|
+
summary: summarizeClaudeStatus(status, mcpEntry),
|
|
181
|
+
nextAction: nextActionFor(status, mcpEntry, sdd?.next),
|
|
182
|
+
checkedPaths,
|
|
183
|
+
canonicalAgentManifest,
|
|
184
|
+
config: { status: configStatus, paths: compactPaths(paths, normalized.payloadMode), mcpEntry: compactMcpEntry(mcpEntry, normalized.payloadMode) },
|
|
185
|
+
...(sdd === undefined ? {} : { sdd: compactSdd(sdd, normalized.payloadMode) }),
|
|
186
|
+
mcpRequiredTools: tools,
|
|
187
|
+
originalBytes: Buffer.byteLength(JSON.stringify(verboseShape), 'utf8'),
|
|
188
|
+
compactBytes: Buffer.byteLength(JSON.stringify(compactShape), 'utf8'),
|
|
189
|
+
verboseAvailable: normalized.payloadMode === 'compact',
|
|
190
|
+
fullContentRef: `provider-status:claude:user-global:${normalized.workspaceRoot}`,
|
|
191
|
+
generatedAt: 'read-only-snapshot',
|
|
192
|
+
safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES },
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
139
196
|
}
|
|
140
197
|
function summarizeClaudeAgents(agents) {
|
|
141
198
|
return { expected: agents.agents.length, managed: agents.agents.filter((agent) => agent.status === 'managed').length, missing: agents.agents.filter((agent) => agent.status === 'missing').length, conflicting: agents.agents.filter((agent) => agent.status === 'conflicting' || agent.status === 'invalid').length };
|
|
@@ -149,6 +206,18 @@ function claudeConfigHealthStatus(statuses) {
|
|
|
149
206
|
return 'warn';
|
|
150
207
|
return 'pass';
|
|
151
208
|
}
|
|
209
|
+
function claudeProjectMemoryPathStatus(state) {
|
|
210
|
+
const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
|
|
211
|
+
return {
|
|
212
|
+
label: 'project CLAUDE.md managed block',
|
|
213
|
+
path: state.path,
|
|
214
|
+
exists: state.exists,
|
|
215
|
+
readable: state.status !== 'blocked' || state.reason !== 'unreadable',
|
|
216
|
+
parsed: state.status === 'managed-current' || state.status === 'managed-stale',
|
|
217
|
+
status,
|
|
218
|
+
detail: state.message,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
152
221
|
function compactPaths(paths, mode) {
|
|
153
222
|
if (mode === 'verbose')
|
|
154
223
|
return paths;
|
package/dist/mcp/schema.js
CHANGED
|
@@ -90,6 +90,7 @@ export function toInternalVgxMcpToolName(toolName) {
|
|
|
90
90
|
return EXPOSED_TO_INTERNAL_TOOL_NAMES[toolName];
|
|
91
91
|
}
|
|
92
92
|
const scopes = ['project', 'personal'];
|
|
93
|
+
const providerScopes = ['project', 'personal', 'local', 'user'];
|
|
93
94
|
const agentModes = ['agent', 'subagent'];
|
|
94
95
|
const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
|
|
95
96
|
const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
|
|
@@ -429,7 +430,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
429
430
|
vgxness_provider_status: z
|
|
430
431
|
.object({
|
|
431
432
|
project: z.string().min(1).optional(),
|
|
432
|
-
scope: z.enum(
|
|
433
|
+
scope: z.enum(providerScopes).optional(),
|
|
433
434
|
providerAdapter: z.enum(['opencode', 'claude']).optional(),
|
|
434
435
|
workspaceRoot: z.string().min(1).optional(),
|
|
435
436
|
change: z.string().min(1).optional(),
|
|
@@ -439,7 +440,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
439
440
|
vgxness_provider_doctor: z
|
|
440
441
|
.object({
|
|
441
442
|
project: z.string().min(1).optional(),
|
|
442
|
-
scope: z.enum(
|
|
443
|
+
scope: z.enum(providerScopes).optional(),
|
|
443
444
|
providerAdapter: z.enum(['opencode', 'claude']).optional(),
|
|
444
445
|
workspaceRoot: z.string().min(1).optional(),
|
|
445
446
|
expectedPromptContractVersion: z.number().int().positive().optional(),
|
|
@@ -449,7 +450,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
449
450
|
vgxness_provider_change_plan: z
|
|
450
451
|
.object({
|
|
451
452
|
project: z.string().min(1).optional(),
|
|
452
|
-
scope: z.enum(
|
|
453
|
+
scope: z.enum(providerScopes).optional(),
|
|
453
454
|
provider: z.enum(providerChangePlanProviders),
|
|
454
455
|
changeType: z.enum(providerChangePlanTypes),
|
|
455
456
|
workspaceRoot: z.string().min(1),
|