cloudflare-mcp-smart-proxy 1.1.1 → 1.3.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/README.md +74 -142
- package/connector-cli.js +144 -0
- package/index.js +37 -3
- package/package.json +7 -3
- package/src/cloud-client.js +135 -0
- package/src/connector-bridge.js +284 -0
- package/src/device-identity.js +87 -0
- package/src/ide-configurator.js +316 -0
- package/src/local-tools.js +84 -2
- package/src/project-probe-discovery.js +137 -0
- package/src/reference-connectors.js +315 -0
- package/src/router.js +81 -20
- package/package-personal.json +0 -28
- package/publish-now.sh +0 -230
- package/publish-with-2fa.sh +0 -134
- package/publish.sh +0 -90
- package/version-bump.sh +0 -128
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CloudClient } from './cloud-client.js';
|
|
4
|
+
import { DeviceIdentity } from './device-identity.js';
|
|
5
|
+
import { discoverProjectProbe } from './project-probe-discovery.js';
|
|
6
|
+
import { detectIde, applyBrainSnapshot } from './ide-configurator.js';
|
|
7
|
+
|
|
8
|
+
function normalizeString(value, fallback = '') {
|
|
9
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
10
|
+
return normalized || fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultConnectorId() {
|
|
14
|
+
return `connector.${os.hostname().replace(/[^a-zA-Z0-9_.-]/g, '_')}.local`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultWorkspaceId(workspaceRoot) {
|
|
18
|
+
return `workspace.${path.basename(workspaceRoot || process.cwd()).replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildIdempotencyKey(...parts) {
|
|
22
|
+
return parts
|
|
23
|
+
.map((entry) => normalizeString(entry))
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join(':');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ConnectorBridge {
|
|
29
|
+
constructor({
|
|
30
|
+
cloudUrl,
|
|
31
|
+
cloudApiKey,
|
|
32
|
+
workspaceRoot,
|
|
33
|
+
clientProfileId,
|
|
34
|
+
connectorId,
|
|
35
|
+
connectorType,
|
|
36
|
+
workspaceId,
|
|
37
|
+
deviceIdentity = null,
|
|
38
|
+
cloudClient = null
|
|
39
|
+
}) {
|
|
40
|
+
this.workspaceRoot = workspaceRoot || process.cwd();
|
|
41
|
+
this.clientProfileId = normalizeString(clientProfileId);
|
|
42
|
+
this.connectorId = normalizeString(connectorId, defaultConnectorId());
|
|
43
|
+
this.connectorType = normalizeString(connectorType, 'smart_proxy');
|
|
44
|
+
this.workspaceId = normalizeString(workspaceId, defaultWorkspaceId(this.workspaceRoot));
|
|
45
|
+
this.deviceIdentity = deviceIdentity || new DeviceIdentity();
|
|
46
|
+
this.cloudClient = cloudClient || new CloudClient({
|
|
47
|
+
cloudUrl,
|
|
48
|
+
cloudApiKey,
|
|
49
|
+
deviceIdentity: this.deviceIdentity
|
|
50
|
+
});
|
|
51
|
+
this.state = {
|
|
52
|
+
initializedAt: Date.now(),
|
|
53
|
+
lastWorkspaceGrant: null,
|
|
54
|
+
lastInstallPlan: null,
|
|
55
|
+
lastStatusReport: null,
|
|
56
|
+
lastProjectProbe: null,
|
|
57
|
+
lastGeneratedContextPack: null,
|
|
58
|
+
lastSyncAt: null,
|
|
59
|
+
lastProjectProbeReportAt: null,
|
|
60
|
+
lastError: null
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isConfigured() {
|
|
65
|
+
return Boolean(this.clientProfileId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getBridgeStatus() {
|
|
69
|
+
return {
|
|
70
|
+
objectType: 'connector_bridge_status',
|
|
71
|
+
configured: this.isConfigured(),
|
|
72
|
+
clientProfileId: this.clientProfileId || null,
|
|
73
|
+
connectorId: this.connectorId,
|
|
74
|
+
connectorType: this.connectorType,
|
|
75
|
+
workspaceId: this.workspaceId,
|
|
76
|
+
workspaceRoot: this.workspaceRoot,
|
|
77
|
+
state: this.state
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async initialize({
|
|
82
|
+
autoSyncProfile = true,
|
|
83
|
+
autoApplyBrain = true,
|
|
84
|
+
autoReportProjectProbe = false,
|
|
85
|
+
autoGenerateContextPack = false
|
|
86
|
+
} = {}) {
|
|
87
|
+
if (!this.isConfigured()) {
|
|
88
|
+
return this.getBridgeStatus();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (typeof this.cloudClient?.registerDevice === 'function') {
|
|
93
|
+
await this.ensureDeviceRegistration();
|
|
94
|
+
}
|
|
95
|
+
const canApplyBrain = autoApplyBrain && typeof this.cloudClient?.fetchBrainSnapshot === 'function';
|
|
96
|
+
if (canApplyBrain) {
|
|
97
|
+
try {
|
|
98
|
+
await this.applyBrain();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!autoSyncProfile) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
await this.syncProfile();
|
|
104
|
+
}
|
|
105
|
+
} else if (autoSyncProfile) {
|
|
106
|
+
await this.syncProfile();
|
|
107
|
+
}
|
|
108
|
+
if (autoReportProjectProbe) {
|
|
109
|
+
await this.reportProjectProbe({ autoGenerateContextPack });
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.state.lastError = error.message;
|
|
113
|
+
}
|
|
114
|
+
return this.getBridgeStatus();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async ensureDeviceRegistration() {
|
|
118
|
+
const identity = await this.deviceIdentity.loadOrCreate();
|
|
119
|
+
const response = await this.cloudClient.registerDevice({
|
|
120
|
+
deviceId: identity.deviceId,
|
|
121
|
+
publicKeyJwk: identity.publicKeyJwk,
|
|
122
|
+
clientProfileId: this.clientProfileId || null,
|
|
123
|
+
connectorId: this.connectorId,
|
|
124
|
+
workspaceId: this.workspaceId,
|
|
125
|
+
label: `${this.connectorType}:${os.hostname()}`
|
|
126
|
+
});
|
|
127
|
+
this.state.deviceBinding = response?.deviceBinding || null;
|
|
128
|
+
this.state.device = response?.device || null;
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fetch the brain snapshot from the cloud and apply it to the local IDE.
|
|
134
|
+
* This is the core of the "cerebral cortex" flow:
|
|
135
|
+
* admin configures skills/rules/backends → connector detects IDE → applies natively.
|
|
136
|
+
*/
|
|
137
|
+
async applyBrain() {
|
|
138
|
+
if (!this.isConfigured()) {
|
|
139
|
+
throw new Error('client profile id is required for brain apply');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const snapshotResponse = await this.cloudClient.fetchBrainSnapshot({
|
|
143
|
+
clientProfileId: this.clientProfileId,
|
|
144
|
+
workspaceId: this.workspaceId
|
|
145
|
+
});
|
|
146
|
+
const snapshot = snapshotResponse?.brainSnapshot;
|
|
147
|
+
if (!snapshot) {
|
|
148
|
+
throw new Error('brain snapshot returned empty response');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.state.lastBrainSnapshot = snapshot;
|
|
152
|
+
this.state.lastBrainAppliedAt = Date.now();
|
|
153
|
+
|
|
154
|
+
const ide = detectIde(this.workspaceRoot);
|
|
155
|
+
const applyResult = await applyBrainSnapshot(snapshot, this.workspaceRoot, ide);
|
|
156
|
+
|
|
157
|
+
this.state.lastBrainApplyResult = applyResult;
|
|
158
|
+
this.state.lastSyncAt = Date.now();
|
|
159
|
+
this.state.lastError = null;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
ide,
|
|
164
|
+
applied: applyResult.applied,
|
|
165
|
+
meta: snapshot.meta
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async syncProfile() {
|
|
170
|
+
if (!this.isConfigured()) {
|
|
171
|
+
throw new Error('client profile id is required for bridge sync');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const workspaceGrantResponse = await this.cloudClient.fetchWorkspaceGrant({
|
|
175
|
+
clientProfileId: this.clientProfileId,
|
|
176
|
+
workspaceId: this.workspaceId,
|
|
177
|
+
connectorId: this.connectorId
|
|
178
|
+
});
|
|
179
|
+
this.state.lastWorkspaceGrant = workspaceGrantResponse?.workspaceGrant || null;
|
|
180
|
+
|
|
181
|
+
const installPlanResponse = await this.cloudClient.fetchInstallPlan({
|
|
182
|
+
clientProfileId: this.clientProfileId,
|
|
183
|
+
workspaceId: this.workspaceId
|
|
184
|
+
});
|
|
185
|
+
this.state.lastInstallPlan = installPlanResponse?.installPlan || null;
|
|
186
|
+
|
|
187
|
+
const statusReportsResponse = await this.cloudClient.listConnectorStatusReports({
|
|
188
|
+
clientProfileId: this.clientProfileId,
|
|
189
|
+
workspaceId: this.workspaceId,
|
|
190
|
+
connectorId: this.connectorId,
|
|
191
|
+
limit: 5
|
|
192
|
+
});
|
|
193
|
+
this.state.lastStatusReport = Array.isArray(statusReportsResponse?.connectorStatusReports)
|
|
194
|
+
? (statusReportsResponse.connectorStatusReports[0] || null)
|
|
195
|
+
: null;
|
|
196
|
+
this.state.lastSyncAt = Date.now();
|
|
197
|
+
|
|
198
|
+
this.state.lastError = null;
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
workspaceGrant: this.state.lastWorkspaceGrant,
|
|
202
|
+
installPlan: this.state.lastInstallPlan,
|
|
203
|
+
lastStatusReport: this.state.lastStatusReport
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async reportStatus({
|
|
208
|
+
status = 'succeeded',
|
|
209
|
+
summary = {},
|
|
210
|
+
operationResults = [],
|
|
211
|
+
issues = []
|
|
212
|
+
} = {}) {
|
|
213
|
+
if (!this.isConfigured()) {
|
|
214
|
+
throw new Error('client profile id is required for status reporting');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const report = {
|
|
218
|
+
idempotencyKey: buildIdempotencyKey(
|
|
219
|
+
'status_report',
|
|
220
|
+
this.clientProfileId,
|
|
221
|
+
this.workspaceId,
|
|
222
|
+
this.connectorId,
|
|
223
|
+
this.state.lastInstallPlan?.id || 'no-plan',
|
|
224
|
+
status
|
|
225
|
+
),
|
|
226
|
+
connectorId: this.connectorId,
|
|
227
|
+
connectorType: this.connectorType,
|
|
228
|
+
workspaceId: this.workspaceId,
|
|
229
|
+
installPlanId: this.state.lastInstallPlan?.id || null,
|
|
230
|
+
status,
|
|
231
|
+
summary,
|
|
232
|
+
operationResults,
|
|
233
|
+
issues
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const response = await this.cloudClient.createConnectorStatusReport({
|
|
237
|
+
clientProfileId: this.clientProfileId,
|
|
238
|
+
report
|
|
239
|
+
});
|
|
240
|
+
this.state.lastStatusReport = response?.connectorStatusReport || null;
|
|
241
|
+
this.state.lastError = null;
|
|
242
|
+
return response;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async discoverProjectProbe() {
|
|
246
|
+
const probe = await discoverProjectProbe({
|
|
247
|
+
workspaceRoot: this.workspaceRoot,
|
|
248
|
+
workspaceId: this.workspaceId,
|
|
249
|
+
clientProfileId: this.clientProfileId || null,
|
|
250
|
+
connectorId: this.connectorId,
|
|
251
|
+
connectorType: this.connectorType
|
|
252
|
+
});
|
|
253
|
+
this.state.lastProjectProbe = probe;
|
|
254
|
+
return probe;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async reportProjectProbe({ autoGenerateContextPack = false } = {}) {
|
|
258
|
+
if (!this.isConfigured()) {
|
|
259
|
+
throw new Error('client profile id is required for project probe reporting');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const probe = await this.discoverProjectProbe();
|
|
263
|
+
const probeWithIdempotency = {
|
|
264
|
+
...probe,
|
|
265
|
+
idempotencyKey: buildIdempotencyKey(
|
|
266
|
+
'project_probe',
|
|
267
|
+
this.clientProfileId,
|
|
268
|
+
this.workspaceId,
|
|
269
|
+
this.connectorId,
|
|
270
|
+
probe.id || probe.repoName || 'workspace'
|
|
271
|
+
)
|
|
272
|
+
};
|
|
273
|
+
const response = await this.cloudClient.createProjectProbe({
|
|
274
|
+
clientProfileId: this.clientProfileId,
|
|
275
|
+
probe: probeWithIdempotency,
|
|
276
|
+
autoGenerateContextPack
|
|
277
|
+
});
|
|
278
|
+
this.state.lastProjectProbe = response?.projectProbe || probe;
|
|
279
|
+
this.state.lastGeneratedContextPack = response?.generatedContextPack || null;
|
|
280
|
+
this.state.lastProjectProbeReportAt = Date.now();
|
|
281
|
+
this.state.lastError = null;
|
|
282
|
+
return response;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { webcrypto } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const subtle = webcrypto.subtle;
|
|
7
|
+
const DEVICE_SIGNATURE_VERSION = 'device_sig_v1';
|
|
8
|
+
|
|
9
|
+
function bytesToBase64(bytes) {
|
|
10
|
+
return Buffer.from(bytes).toString('base64');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function sha256Hex(value) {
|
|
14
|
+
const digest = await subtle.digest('SHA-256', new TextEncoder().encode(String(value || '')));
|
|
15
|
+
return Buffer.from(digest).toString('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function defaultIdentityPath() {
|
|
19
|
+
return path.join(os.homedir(), '.cloudmcp', 'device-identity.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ensureParent(filePath) {
|
|
23
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class DeviceIdentity {
|
|
27
|
+
constructor({ identityPath = defaultIdentityPath() } = {}) {
|
|
28
|
+
this.identityPath = identityPath;
|
|
29
|
+
this.record = null;
|
|
30
|
+
this.privateKey = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async loadOrCreate() {
|
|
34
|
+
if (this.record && this.privateKey) return this.record;
|
|
35
|
+
if (fs.existsSync(this.identityPath)) {
|
|
36
|
+
this.record = JSON.parse(fs.readFileSync(this.identityPath, 'utf8'));
|
|
37
|
+
} else {
|
|
38
|
+
const keyPair = await subtle.generateKey(
|
|
39
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
40
|
+
true,
|
|
41
|
+
['sign', 'verify']
|
|
42
|
+
);
|
|
43
|
+
this.record = {
|
|
44
|
+
deviceId: `dev_${crypto.randomUUID()}`,
|
|
45
|
+
publicKeyJwk: await subtle.exportKey('jwk', keyPair.publicKey),
|
|
46
|
+
privateKeyJwk: await subtle.exportKey('jwk', keyPair.privateKey),
|
|
47
|
+
createdAt: Date.now()
|
|
48
|
+
};
|
|
49
|
+
ensureParent(this.identityPath);
|
|
50
|
+
fs.writeFileSync(this.identityPath, `${JSON.stringify(this.record, null, 2)}\n`, { mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
this.privateKey = await subtle.importKey(
|
|
53
|
+
'jwk',
|
|
54
|
+
this.record.privateKeyJwk,
|
|
55
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
56
|
+
false,
|
|
57
|
+
['sign']
|
|
58
|
+
);
|
|
59
|
+
return this.record;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async buildSignedHeaders({ method, pathname, body = '' }) {
|
|
63
|
+
const record = await this.loadOrCreate();
|
|
64
|
+
const timestamp = String(Date.now());
|
|
65
|
+
const nonce = crypto.randomUUID();
|
|
66
|
+
const bodyHash = await sha256Hex(body);
|
|
67
|
+
const canonical = [
|
|
68
|
+
DEVICE_SIGNATURE_VERSION,
|
|
69
|
+
String(method || 'GET').toUpperCase(),
|
|
70
|
+
pathname || '/',
|
|
71
|
+
bodyHash,
|
|
72
|
+
timestamp,
|
|
73
|
+
nonce
|
|
74
|
+
].join('\n');
|
|
75
|
+
const signature = await subtle.sign(
|
|
76
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
77
|
+
this.privateKey,
|
|
78
|
+
new TextEncoder().encode(canonical)
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
'X-CloudMCP-Device-ID': record.deviceId,
|
|
82
|
+
'X-CloudMCP-Device-Timestamp': timestamp,
|
|
83
|
+
'X-CloudMCP-Device-Nonce': nonce,
|
|
84
|
+
'X-CloudMCP-Device-Signature': bytesToBase64(signature)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ide-configurator.js
|
|
3
|
+
*
|
|
4
|
+
* Detects which IDE the connector is running inside, then applies
|
|
5
|
+
* a brain snapshot natively (rules files, MCP server config, context files).
|
|
6
|
+
*
|
|
7
|
+
* Supported IDEs:
|
|
8
|
+
* codex — Codex CLI (~/.codex/config.toml)
|
|
9
|
+
* cursor — Cursor editor (.cursor/rules + ~/.cursor/mcp.json)
|
|
10
|
+
* claude_code — Claude Code (.mcp.json / ~/.claude.json)
|
|
11
|
+
* claude — Claude Desktop legacy (~/Library/…/claude_desktop_config.json)
|
|
12
|
+
* vscode — VS Code Copilot (.github/copilot-instructions.md + .vscode/mcp.json)
|
|
13
|
+
* unknown — fallback: only write project-local files if possible
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { mergeCodexConfig, mergeClaudeCodeConfig } from './reference-connectors.js';
|
|
20
|
+
|
|
21
|
+
// ── IDE detection ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the IDE identifier string.
|
|
25
|
+
* Priority: explicit env override → process/env signals → filesystem signals.
|
|
26
|
+
*/
|
|
27
|
+
export function detectIde(workspaceRoot) {
|
|
28
|
+
// 1. Explicit override from env (useful in non-standard setups)
|
|
29
|
+
const override = (process.env.CLOUDMCP_IDE || '').trim().toLowerCase();
|
|
30
|
+
if (['codex', 'cursor', 'claude_code', 'claude', 'vscode'].includes(override)) {
|
|
31
|
+
return override;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Codex-specific env vars should win over the VSCode host shell it often runs inside
|
|
35
|
+
if (_isCodex()) return 'codex';
|
|
36
|
+
|
|
37
|
+
// 3. Claude Code project markers
|
|
38
|
+
if (_isClaudeCode(workspaceRoot)) return 'claude_code';
|
|
39
|
+
|
|
40
|
+
// 4. VSCode family: reliable env var
|
|
41
|
+
if (process.env.VSCODE_PID || process.env.VSCODE_IPC_HOOK || process.env.TERM_PROGRAM === 'vscode') {
|
|
42
|
+
// Distinguish Cursor (a VSCode fork) from vanilla VSCode
|
|
43
|
+
if (_isCursor()) return 'cursor';
|
|
44
|
+
return 'vscode';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 5. Cursor-specific env vars (set by Cursor launcher)
|
|
48
|
+
if (_isCursor()) return 'cursor';
|
|
49
|
+
|
|
50
|
+
// 6. Claude Desktop legacy — checks parent process name or known env var
|
|
51
|
+
if (process.env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_DESKTOP_APP) {
|
|
52
|
+
return 'claude';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 7. Filesystem heuristic: if workspace has a .cursor directory, assume Cursor
|
|
56
|
+
if (workspaceRoot && fs.existsSync(path.join(workspaceRoot, '.cursor'))) {
|
|
57
|
+
return 'cursor';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 8. Fallback
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _isCodex() {
|
|
65
|
+
return !!(
|
|
66
|
+
process.env.CODEX_CI ||
|
|
67
|
+
process.env.CODEX_THREAD_ID ||
|
|
68
|
+
process.env.CODEX_HOME ||
|
|
69
|
+
(process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || '').toLowerCase().includes('codex')
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _isCursor() {
|
|
74
|
+
return !!(
|
|
75
|
+
process.env.CURSOR_TRACE ||
|
|
76
|
+
process.env.CURSOR_CHANNEL ||
|
|
77
|
+
process.env.CURSOR_SESSION_ID ||
|
|
78
|
+
// Cursor sets GIT_ASKPASS to a cursor-specific binary
|
|
79
|
+
(process.env.GIT_ASKPASS || '').toLowerCase().includes('cursor')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _isClaudeCode(workspaceRoot) {
|
|
84
|
+
return !!(
|
|
85
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
86
|
+
process.env.CLAUDE_CODE_ENTRYPOINT ||
|
|
87
|
+
(workspaceRoot && fs.existsSync(path.join(workspaceRoot, '.claude')))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Brain snapshot application ────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply a brain snapshot to the detected IDE.
|
|
95
|
+
* Returns a summary of what was written.
|
|
96
|
+
*/
|
|
97
|
+
export async function applyBrainSnapshot(snapshot, workspaceRoot, ide = null) {
|
|
98
|
+
const detectedIde = ide || detectIde(workspaceRoot);
|
|
99
|
+
const results = [];
|
|
100
|
+
|
|
101
|
+
switch (detectedIde) {
|
|
102
|
+
case 'codex':
|
|
103
|
+
results.push(..._applyCodex(snapshot, workspaceRoot));
|
|
104
|
+
break;
|
|
105
|
+
case 'cursor':
|
|
106
|
+
results.push(..._applyCursor(snapshot, workspaceRoot));
|
|
107
|
+
break;
|
|
108
|
+
case 'claude_code':
|
|
109
|
+
results.push(..._applyClaudeCode(snapshot, workspaceRoot));
|
|
110
|
+
break;
|
|
111
|
+
case 'claude':
|
|
112
|
+
results.push(..._applyClaude(snapshot, workspaceRoot));
|
|
113
|
+
break;
|
|
114
|
+
case 'vscode':
|
|
115
|
+
results.push(..._applyVSCode(snapshot, workspaceRoot));
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
// Unknown IDE: write project-local files only (no global config touched)
|
|
119
|
+
results.push(..._applyLocal(snapshot, workspaceRoot));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { ide: detectedIde, applied: results };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Codex ─────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function _applyCodex(snapshot, workspaceRoot) {
|
|
129
|
+
const results = [];
|
|
130
|
+
const mcpServers = snapshot.mcpServers || {};
|
|
131
|
+
|
|
132
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
133
|
+
const configDir = path.join(os.homedir(), '.codex');
|
|
134
|
+
const configFile = path.join(configDir, 'config.toml');
|
|
135
|
+
_ensureDir(configFile);
|
|
136
|
+
let content = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf8') : '';
|
|
137
|
+
for (const [serverName, serverDefinition] of Object.entries(mcpServers)) {
|
|
138
|
+
content = mergeCodexConfig(content, serverName, {
|
|
139
|
+
...serverDefinition,
|
|
140
|
+
enabled: serverDefinition?.enabled !== false
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
fs.writeFileSync(configFile, content, 'utf8');
|
|
144
|
+
results.push({ action: 'merged_mcp', file: configFile, count: Object.keys(mcpServers).length });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
results.push(..._writeContextEntries(snapshot.contextEntries, workspaceRoot, '.cloudmcp/codex'));
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Cursor ────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function _applyCursor(snapshot, workspaceRoot) {
|
|
154
|
+
const results = [];
|
|
155
|
+
|
|
156
|
+
// 1. System prompt → .cursor/rules/cloudmcp.md (project-local, alwaysApply)
|
|
157
|
+
if (snapshot.systemPrompt) {
|
|
158
|
+
const rulesDir = path.join(workspaceRoot, '.cursor', 'rules');
|
|
159
|
+
_ensureDir(rulesDir);
|
|
160
|
+
const rulesFile = path.join(rulesDir, 'cloudmcp.md');
|
|
161
|
+
const content = [
|
|
162
|
+
'---',
|
|
163
|
+
'description: CloudMCP brain — auto-applied by connector, do not edit manually',
|
|
164
|
+
'alwaysApply: true',
|
|
165
|
+
'---',
|
|
166
|
+
'',
|
|
167
|
+
snapshot.systemPrompt
|
|
168
|
+
].join('\n');
|
|
169
|
+
fs.writeFileSync(rulesFile, content, 'utf8');
|
|
170
|
+
results.push({ action: 'wrote', file: rulesFile });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Additional MCP backends → merge into ~/.cursor/mcp.json
|
|
174
|
+
// (the proxy itself is already registered; this adds extra backends from the profile)
|
|
175
|
+
const mcpServers = snapshot.mcpServers || {};
|
|
176
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
177
|
+
const cursorConfigDir = path.join(os.homedir(), '.cursor');
|
|
178
|
+
_ensureDir(cursorConfigDir);
|
|
179
|
+
const mcpFile = path.join(cursorConfigDir, 'mcp.json');
|
|
180
|
+
const existing = _readJson(mcpFile, { mcpServers: {} });
|
|
181
|
+
existing.mcpServers = { ...(existing.mcpServers || {}), ...mcpServers };
|
|
182
|
+
fs.writeFileSync(mcpFile, JSON.stringify(existing, null, 2), 'utf8');
|
|
183
|
+
results.push({ action: 'merged_mcp', file: mcpFile, count: Object.keys(mcpServers).length });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. Context entries → .cursor/context/cloudmcp-context.md
|
|
187
|
+
results.push(..._writeContextEntries(snapshot.contextEntries, workspaceRoot, '.cursor/context'));
|
|
188
|
+
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Claude Code ───────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function _applyClaudeCode(snapshot, workspaceRoot) {
|
|
195
|
+
const results = [];
|
|
196
|
+
const mcpServers = snapshot.mcpServers || {};
|
|
197
|
+
|
|
198
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
199
|
+
const configFile = path.join(workspaceRoot, '.mcp.json');
|
|
200
|
+
const existing = _readJson(configFile, {});
|
|
201
|
+
let nextConfig = existing;
|
|
202
|
+
for (const [serverName, serverDefinition] of Object.entries(mcpServers)) {
|
|
203
|
+
nextConfig = mergeClaudeCodeConfig(nextConfig, serverName, serverDefinition);
|
|
204
|
+
}
|
|
205
|
+
_ensureDir(configFile);
|
|
206
|
+
fs.writeFileSync(configFile, JSON.stringify(nextConfig, null, 2), 'utf8');
|
|
207
|
+
results.push({ action: 'merged_mcp', file: configFile, count: Object.keys(mcpServers).length });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
results.push(..._writeContextEntries(snapshot.contextEntries, workspaceRoot, '.cloudmcp/claude-code'));
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Claude Desktop ────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
function _applyClaude(snapshot, workspaceRoot) {
|
|
217
|
+
const results = [];
|
|
218
|
+
|
|
219
|
+
// Claude Desktop MCP config location
|
|
220
|
+
let configDir;
|
|
221
|
+
if (process.platform === 'darwin') {
|
|
222
|
+
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
|
|
223
|
+
} else if (process.platform === 'win32') {
|
|
224
|
+
configDir = path.join(process.env.APPDATA || os.homedir(), 'Claude');
|
|
225
|
+
} else {
|
|
226
|
+
configDir = path.join(os.homedir(), '.config', 'Claude');
|
|
227
|
+
}
|
|
228
|
+
_ensureDir(configDir);
|
|
229
|
+
|
|
230
|
+
// Merge MCP servers
|
|
231
|
+
const mcpServers = snapshot.mcpServers || {};
|
|
232
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
233
|
+
const configFile = path.join(configDir, 'claude_desktop_config.json');
|
|
234
|
+
const existing = _readJson(configFile, { mcpServers: {} });
|
|
235
|
+
existing.mcpServers = { ...(existing.mcpServers || {}), ...mcpServers };
|
|
236
|
+
fs.writeFileSync(configFile, JSON.stringify(existing, null, 2), 'utf8');
|
|
237
|
+
results.push({ action: 'merged_mcp', file: configFile, count: Object.keys(mcpServers).length });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Claude Desktop doesn't have a rules file; write context as project-local markdown
|
|
241
|
+
results.push(..._writeContextEntries(snapshot.contextEntries, workspaceRoot, '.cloudmcp'));
|
|
242
|
+
|
|
243
|
+
return results;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── VS Code Copilot ───────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function _applyVSCode(snapshot, workspaceRoot) {
|
|
249
|
+
const results = [];
|
|
250
|
+
|
|
251
|
+
// 1. System prompt → .github/copilot-instructions.md
|
|
252
|
+
if (snapshot.systemPrompt) {
|
|
253
|
+
const ghDir = path.join(workspaceRoot, '.github');
|
|
254
|
+
_ensureDir(ghDir);
|
|
255
|
+
const instructionsFile = path.join(ghDir, 'copilot-instructions.md');
|
|
256
|
+
fs.writeFileSync(instructionsFile, snapshot.systemPrompt, 'utf8');
|
|
257
|
+
results.push({ action: 'wrote', file: instructionsFile });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2. MCP servers → .vscode/mcp.json
|
|
261
|
+
const mcpServers = snapshot.mcpServers || {};
|
|
262
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
263
|
+
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
|
264
|
+
_ensureDir(vscodeDir);
|
|
265
|
+
const mcpFile = path.join(vscodeDir, 'mcp.json');
|
|
266
|
+
const existing = _readJson(mcpFile, { servers: {} });
|
|
267
|
+
// VSCode uses "servers" not "mcpServers"
|
|
268
|
+
existing.servers = { ...(existing.servers || {}), ...mcpServers };
|
|
269
|
+
fs.writeFileSync(mcpFile, JSON.stringify(existing, null, 2), 'utf8');
|
|
270
|
+
results.push({ action: 'merged_mcp', file: mcpFile, count: Object.keys(mcpServers).length });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 3. Context entries
|
|
274
|
+
results.push(..._writeContextEntries(snapshot.contextEntries, workspaceRoot, '.vscode/cloudmcp'));
|
|
275
|
+
|
|
276
|
+
return results;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Fallback (unknown IDE) ────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function _applyLocal(snapshot, workspaceRoot) {
|
|
282
|
+
return _writeContextEntries(snapshot.contextEntries, workspaceRoot, '.cloudmcp');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
function _writeContextEntries(entries, workspaceRoot, subDir) {
|
|
288
|
+
if (!Array.isArray(entries) || entries.length === 0) return [];
|
|
289
|
+
const results = [];
|
|
290
|
+
const dir = path.join(workspaceRoot, subDir);
|
|
291
|
+
_ensureDir(dir);
|
|
292
|
+
const outFile = path.join(dir, 'context.md');
|
|
293
|
+
const lines = ['# CloudMCP Project Context', ''];
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
if (!entry?.content) continue;
|
|
296
|
+
lines.push(`## ${entry.title || entry.entryId || 'Context'}`, '', entry.content, '');
|
|
297
|
+
}
|
|
298
|
+
fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
|
|
299
|
+
results.push({ action: 'wrote', file: outFile, count: entries.length });
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _ensureDir(dir) {
|
|
304
|
+
const targetDir = path.extname(dir) ? path.dirname(dir) : dir;
|
|
305
|
+
if (!fs.existsSync(targetDir)) {
|
|
306
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function _readJson(filePath, fallback) {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
313
|
+
} catch {
|
|
314
|
+
return fallback;
|
|
315
|
+
}
|
|
316
|
+
}
|