fraim-framework 2.0.177 → 2.0.179
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/src/ai-hub/server.js +50 -1
- package/dist/src/cli/commands/add-provider.js +74 -61
- package/dist/src/cli/commands/add-surface.js +128 -0
- package/dist/src/cli/commands/login.js +5 -69
- package/dist/src/cli/commands/setup.js +27 -347
- package/dist/src/cli/distribution/marketplace-bundles.js +576 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +5 -3
- package/dist/src/cli/mcp/mcp-server-registry.js +10 -3
- package/dist/src/cli/providers/local-provider-registry.js +2 -3
- package/dist/src/cli/setup/auto-mcp-setup.js +9 -32
- package/dist/src/cli/setup/ide-detector.js +34 -14
- package/dist/src/config/persona-capability-bundles.js +17 -13
- package/dist/src/first-run/session-service.js +2 -2
- package/dist/src/local-mcp-server/stdio-server.js +28 -4
- package/dist/src/local-mcp-server/usage-collector.js +24 -0
- package/package.json +3 -2
- package/public/ai-hub/index.html +14 -2
- package/public/ai-hub/script.js +340 -66
- package/public/ai-hub/styles.css +83 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateMarketplaceBundles = validateMarketplaceBundles;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const TARGET_ROOT = path_1.default.join('marketplaces', 'fraim');
|
|
10
|
+
const ACCEPTANCE_EVIDENCE_PATH = path_1.default.join('docs', 'evidence', '674-marketplace-acceptance.md');
|
|
11
|
+
const POLICY_PAGE_PATHS = [
|
|
12
|
+
path_1.default.join('fraim-pro', 'privacy', 'index.html'),
|
|
13
|
+
path_1.default.join('fraim-pro', 'terms', 'index.html')
|
|
14
|
+
];
|
|
15
|
+
const ROOT_CURSOR_MARKETPLACE_PATH = path_1.default.join('.cursor-plugin', 'marketplace.json');
|
|
16
|
+
const ROOT_COPILOT_MARKETPLACE_PATH = path_1.default.join('.github', 'plugin', 'marketplace.json');
|
|
17
|
+
const REQUIRED_TARGET_IDS = [
|
|
18
|
+
'openai-apps',
|
|
19
|
+
'codex-plugin',
|
|
20
|
+
'official-mcp-registry',
|
|
21
|
+
'cursor-plugin',
|
|
22
|
+
'vscode-copilot-agent-plugin',
|
|
23
|
+
'gemini-cli-extension'
|
|
24
|
+
];
|
|
25
|
+
const ACCEPTED_LOCAL_STATUSES = new Set(['package-prepared', 'submission-ready', 'submitted']);
|
|
26
|
+
const TEXT_EXTENSIONS = new Set(['.json', '.md', '.txt', '.toml', '.yaml', '.yml']);
|
|
27
|
+
const SECRET_PATTERNS = [
|
|
28
|
+
{ label: 'OpenAI-style API key', pattern: /sk-[A-Za-z0-9_-]{20,}/ },
|
|
29
|
+
{ label: 'FRAIM API key assignment', pattern: /FRAIM_API_KEY\s*[:=]/i },
|
|
30
|
+
{ label: 'bearer authorization header', pattern: /Authorization:\s*Bearer/i },
|
|
31
|
+
{ label: 'stored access token value', pattern: /"access_token"\s*:\s*"[A-Za-z0-9_-]{8,}"/i },
|
|
32
|
+
];
|
|
33
|
+
function normalizeRelPath(value) {
|
|
34
|
+
return value.replace(/\\/g, '/');
|
|
35
|
+
}
|
|
36
|
+
function toRelPath(repoRoot, absolutePath) {
|
|
37
|
+
return normalizeRelPath(path_1.default.relative(repoRoot, absolutePath));
|
|
38
|
+
}
|
|
39
|
+
function makeResult() {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
targets: [],
|
|
43
|
+
checkedFiles: [],
|
|
44
|
+
errors: [],
|
|
45
|
+
warnings: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function addIssue(issues, filePath, message) {
|
|
49
|
+
issues.push({ path: normalizeRelPath(filePath), message });
|
|
50
|
+
}
|
|
51
|
+
function markChecked(result, relPath) {
|
|
52
|
+
const normalized = normalizeRelPath(relPath);
|
|
53
|
+
if (!result.checkedFiles.includes(normalized)) {
|
|
54
|
+
result.checkedFiles.push(normalized);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveRepoPath(repoRoot, relPath) {
|
|
58
|
+
return path_1.default.resolve(repoRoot, relPath);
|
|
59
|
+
}
|
|
60
|
+
function assertFile(repoRoot, relPath, result) {
|
|
61
|
+
markChecked(result, relPath);
|
|
62
|
+
const absolutePath = resolveRepoPath(repoRoot, relPath);
|
|
63
|
+
if (!(0, fs_1.existsSync)(absolutePath) || !(0, fs_1.statSync)(absolutePath).isFile()) {
|
|
64
|
+
addIssue(result.errors, relPath, 'required file is missing');
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function readJsonObject(repoRoot, relPath, result) {
|
|
70
|
+
if (!assertFile(repoRoot, relPath, result)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(resolveRepoPath(repoRoot, relPath), 'utf8'));
|
|
75
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
76
|
+
addIssue(result.errors, relPath, 'must contain a JSON object');
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
addIssue(result.errors, relPath, `invalid JSON: ${message}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function asObject(value) {
|
|
88
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
89
|
+
? value
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
function asArray(value) {
|
|
93
|
+
return Array.isArray(value) ? value : [];
|
|
94
|
+
}
|
|
95
|
+
function stringValue(value) {
|
|
96
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
97
|
+
}
|
|
98
|
+
function readPackageVersion(repoRoot, result) {
|
|
99
|
+
const relPath = 'package.json';
|
|
100
|
+
const packageJson = readJsonObject(repoRoot, relPath, result);
|
|
101
|
+
return packageJson ? stringValue(packageJson.version) : null;
|
|
102
|
+
}
|
|
103
|
+
function requireHttpsUrl(value, relPath, field, result) {
|
|
104
|
+
const url = stringValue(value);
|
|
105
|
+
if (!url) {
|
|
106
|
+
addIssue(result.errors, relPath, `${field} must be a non-empty string`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (!url.startsWith('https://')) {
|
|
110
|
+
addIssue(result.errors, relPath, `${field} must be an https URL`);
|
|
111
|
+
}
|
|
112
|
+
return url;
|
|
113
|
+
}
|
|
114
|
+
function assertEqual(actual, expected, relPath, field, result) {
|
|
115
|
+
if (actual !== expected) {
|
|
116
|
+
addIssue(result.errors, relPath, `${field} must be ${JSON.stringify(expected)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function assertRelativeAsset(repoRoot, baseRelPath, rawPath, relPath, field, result) {
|
|
120
|
+
const assetPath = stringValue(rawPath);
|
|
121
|
+
if (!assetPath) {
|
|
122
|
+
addIssue(result.errors, relPath, `${field} must be a non-empty relative path`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const normalized = assetPath.replace(/\\/g, '/');
|
|
126
|
+
if (path_1.default.posix.isAbsolute(normalized) || normalized.split('/').includes('..')) {
|
|
127
|
+
addIssue(result.errors, relPath, `${field} must stay inside the package`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const absolutePath = resolveRepoPath(repoRoot, path_1.default.join(baseRelPath, normalized));
|
|
131
|
+
const targetRelPath = toRelPath(repoRoot, absolutePath);
|
|
132
|
+
markChecked(result, targetRelPath);
|
|
133
|
+
if (!(0, fs_1.existsSync)(absolutePath) || !(0, fs_1.statSync)(absolutePath).isFile()) {
|
|
134
|
+
addIssue(result.errors, relPath, `${field} points to missing file ${normalized}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function walkTextFiles(basePath) {
|
|
138
|
+
if (!(0, fs_1.existsSync)(basePath))
|
|
139
|
+
return [];
|
|
140
|
+
const files = [];
|
|
141
|
+
for (const entry of (0, fs_1.readdirSync)(basePath, { withFileTypes: true })) {
|
|
142
|
+
const fullPath = path_1.default.join(basePath, entry.name);
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
files.push(...walkTextFiles(fullPath));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (entry.isFile() && TEXT_EXTENSIONS.has(path_1.default.extname(entry.name).toLowerCase())) {
|
|
148
|
+
files.push(fullPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
function scanForSecrets(repoRoot, relPath, result) {
|
|
154
|
+
const absolutePath = resolveRepoPath(repoRoot, relPath);
|
|
155
|
+
const files = (0, fs_1.existsSync)(absolutePath) && (0, fs_1.statSync)(absolutePath).isDirectory()
|
|
156
|
+
? walkTextFiles(absolutePath)
|
|
157
|
+
: [absolutePath];
|
|
158
|
+
for (const filePath of files) {
|
|
159
|
+
if (!(0, fs_1.existsSync)(filePath) || !(0, fs_1.statSync)(filePath).isFile())
|
|
160
|
+
continue;
|
|
161
|
+
if (!TEXT_EXTENSIONS.has(path_1.default.extname(filePath).toLowerCase()))
|
|
162
|
+
continue;
|
|
163
|
+
const fileRelPath = toRelPath(repoRoot, filePath);
|
|
164
|
+
markChecked(result, fileRelPath);
|
|
165
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
166
|
+
for (const secretPattern of SECRET_PATTERNS) {
|
|
167
|
+
if (secretPattern.pattern.test(content)) {
|
|
168
|
+
addIssue(result.errors, fileRelPath, `contains possible secret: ${secretPattern.label}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function validateTargetEntries(repoRoot, relPath, targets, result) {
|
|
174
|
+
result.targets = targets
|
|
175
|
+
.map((target) => asObject(target)?.id)
|
|
176
|
+
.filter((id) => typeof id === 'string');
|
|
177
|
+
for (const requiredTargetId of REQUIRED_TARGET_IDS) {
|
|
178
|
+
if (!result.targets.includes(requiredTargetId)) {
|
|
179
|
+
addIssue(result.errors, relPath, `missing target ${requiredTargetId}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const target of targets) {
|
|
183
|
+
const targetObject = asObject(target);
|
|
184
|
+
if (!targetObject)
|
|
185
|
+
continue;
|
|
186
|
+
const targetId = stringValue(targetObject.id) || '<unknown>';
|
|
187
|
+
const packagePath = stringValue(targetObject.packagePath);
|
|
188
|
+
if (!packagePath) {
|
|
189
|
+
addIssue(result.errors, relPath, `target ${targetId} packagePath must be set`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!(0, fs_1.existsSync)(resolveRepoPath(repoRoot, packagePath))) {
|
|
193
|
+
addIssue(result.errors, relPath, `target ${targetId} packagePath does not exist`);
|
|
194
|
+
}
|
|
195
|
+
const status = stringValue(targetObject.status);
|
|
196
|
+
if (!status || !ACCEPTED_LOCAL_STATUSES.has(status)) {
|
|
197
|
+
addIssue(result.warnings, relPath, `target ${targetId} has unexpected status ${JSON.stringify(targetObject.status)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function validateTargetMatrix(repoRoot, result) {
|
|
202
|
+
const relPath = path_1.default.join(TARGET_ROOT, 'marketplace-targets.json');
|
|
203
|
+
const targetMatrix = readJsonObject(repoRoot, relPath, result);
|
|
204
|
+
if (!targetMatrix) {
|
|
205
|
+
return { remoteMcpUrl: null, oauthAuthorizationServer: null, productVersion: null };
|
|
206
|
+
}
|
|
207
|
+
assertEqual(targetMatrix.schema, 'fraim-marketplace-targets-v1', relPath, 'schema', result);
|
|
208
|
+
assertEqual(targetMatrix.issue, 674, relPath, 'issue', result);
|
|
209
|
+
const product = asObject(targetMatrix.product);
|
|
210
|
+
const remoteMcp = asObject(targetMatrix.remoteMcp);
|
|
211
|
+
if (!product) {
|
|
212
|
+
addIssue(result.errors, relPath, 'product must be an object');
|
|
213
|
+
}
|
|
214
|
+
if (!remoteMcp) {
|
|
215
|
+
addIssue(result.errors, relPath, 'remoteMcp must be an object');
|
|
216
|
+
}
|
|
217
|
+
const productVersion = product ? stringValue(product.packageVersion) : null;
|
|
218
|
+
if (!productVersion) {
|
|
219
|
+
addIssue(result.errors, relPath, 'product.packageVersion must be set');
|
|
220
|
+
}
|
|
221
|
+
const packageVersion = readPackageVersion(repoRoot, result);
|
|
222
|
+
if (productVersion && packageVersion) {
|
|
223
|
+
assertEqual(productVersion, packageVersion, relPath, 'product.packageVersion', result);
|
|
224
|
+
}
|
|
225
|
+
const remoteMcpUrl = remoteMcp
|
|
226
|
+
? requireHttpsUrl(remoteMcp.url, relPath, 'remoteMcp.url', result)
|
|
227
|
+
: null;
|
|
228
|
+
const oauthAuthorizationServer = remoteMcp
|
|
229
|
+
? requireHttpsUrl(remoteMcp.oauthAuthorizationServer, relPath, 'remoteMcp.oauthAuthorizationServer', result)
|
|
230
|
+
: null;
|
|
231
|
+
validateTargetEntries(repoRoot, relPath, asArray(targetMatrix.targets), result);
|
|
232
|
+
return { remoteMcpUrl, oauthAuthorizationServer, productVersion };
|
|
233
|
+
}
|
|
234
|
+
function validateOpenAiAssets(repoRoot, openAiRoot, result) {
|
|
235
|
+
assertFile(repoRoot, path_1.default.join(openAiRoot, 'README.md'), result);
|
|
236
|
+
assertFile(repoRoot, path_1.default.join(openAiRoot, 'review-checklist.md'), result);
|
|
237
|
+
for (const asset of [
|
|
238
|
+
'assets/icon-512.png',
|
|
239
|
+
'assets/logo.png',
|
|
240
|
+
'assets/screenshot-workspace.png',
|
|
241
|
+
'assets/screenshot-signin.png',
|
|
242
|
+
'assets/screenshot-delegation.png'
|
|
243
|
+
]) {
|
|
244
|
+
assertFile(repoRoot, path_1.default.join(openAiRoot, asset), result);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function validateOpenAiPacketFields(packet, relPath, targetDetails, result) {
|
|
248
|
+
const app = asObject(packet.app);
|
|
249
|
+
const mcp = asObject(packet.mcp);
|
|
250
|
+
const reviewNotes = asObject(packet.reviewNotes);
|
|
251
|
+
if (!app) {
|
|
252
|
+
addIssue(result.errors, relPath, 'app must be an object');
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
assertEqual(app.name, 'FRAIM', relPath, 'app.name', result);
|
|
256
|
+
requireHttpsUrl(app.website, relPath, 'app.website', result);
|
|
257
|
+
requireHttpsUrl(app.privacyPolicy, relPath, 'app.privacyPolicy', result);
|
|
258
|
+
requireHttpsUrl(app.termsOfService, relPath, 'app.termsOfService', result);
|
|
259
|
+
}
|
|
260
|
+
if (!mcp) {
|
|
261
|
+
addIssue(result.errors, relPath, 'mcp must be an object');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
assertEqual(mcp.serverUrl, targetDetails.remoteMcpUrl, relPath, 'mcp.serverUrl', result);
|
|
265
|
+
assertEqual(mcp.authorizationServer, targetDetails.oauthAuthorizationServer, relPath, 'mcp.authorizationServer', result);
|
|
266
|
+
const auth = asObject(mcp.auth);
|
|
267
|
+
assertEqual(auth?.type, 'OAuth 2.1 PKCE', relPath, 'mcp.auth.type', result);
|
|
268
|
+
}
|
|
269
|
+
if (!reviewNotes) {
|
|
270
|
+
addIssue(result.errors, relPath, 'reviewNotes must be an object');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
assertEqual(reviewNotes.credentialsStoredInRepo, false, relPath, 'reviewNotes.credentialsStoredInRepo', result);
|
|
274
|
+
assertEqual(reviewNotes.reviewerDemoAccountRequired, true, relPath, 'reviewNotes.reviewerDemoAccountRequired', result);
|
|
275
|
+
}
|
|
276
|
+
const testCases = asArray(packet.testCases);
|
|
277
|
+
if (testCases.length < 3) {
|
|
278
|
+
addIssue(result.errors, relPath, 'at least three reviewer test cases are required');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function validateOpenAiPacket(repoRoot, result, targetDetails) {
|
|
282
|
+
const openAiRoot = path_1.default.join(TARGET_ROOT, 'openai');
|
|
283
|
+
const relPath = path_1.default.join(openAiRoot, 'submission-packet.json');
|
|
284
|
+
const packet = readJsonObject(repoRoot, relPath, result);
|
|
285
|
+
validateOpenAiAssets(repoRoot, openAiRoot, result);
|
|
286
|
+
if (!packet)
|
|
287
|
+
return;
|
|
288
|
+
assertEqual(packet.schema, 'fraim-openai-app-submission-packet-v1', relPath, 'schema', result);
|
|
289
|
+
validateOpenAiPacketFields(packet, relPath, targetDetails, result);
|
|
290
|
+
}
|
|
291
|
+
function validatePolicyPages(repoRoot, result) {
|
|
292
|
+
for (const relPath of POLICY_PAGE_PATHS) {
|
|
293
|
+
if (!assertFile(repoRoot, relPath, result)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const content = (0, fs_1.readFileSync)(resolveRepoPath(repoRoot, relPath), 'utf8');
|
|
297
|
+
if (!content.includes('FRAIM')) {
|
|
298
|
+
addIssue(result.errors, relPath, 'policy page must identify FRAIM');
|
|
299
|
+
}
|
|
300
|
+
if (!content.includes('support@fraimworks.ai')) {
|
|
301
|
+
addIssue(result.errors, relPath, 'policy page must include support contact');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function validateCodexMarketplace(marketplace, marketplaceRelPath, result) {
|
|
306
|
+
if (!marketplace)
|
|
307
|
+
return;
|
|
308
|
+
assertEqual(marketplace.name, 'fraim-openai-codex', marketplaceRelPath, 'name', result);
|
|
309
|
+
const entries = asArray(marketplace.plugins);
|
|
310
|
+
const fraimEntry = entries
|
|
311
|
+
.map((entry) => asObject(entry))
|
|
312
|
+
.find((entry) => entry?.name === 'fraim');
|
|
313
|
+
if (!fraimEntry) {
|
|
314
|
+
addIssue(result.errors, marketplaceRelPath, 'plugins must include fraim');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const source = asObject(fraimEntry.source);
|
|
318
|
+
assertEqual(source?.source, 'local', marketplaceRelPath, 'fraim.source.source', result);
|
|
319
|
+
assertEqual(source?.path, './plugins/fraim', marketplaceRelPath, 'fraim.source.path', result);
|
|
320
|
+
}
|
|
321
|
+
function validateCodexManifestInterface(repoRoot, pluginRoot, manifestRelPath, manifestInterface, result) {
|
|
322
|
+
requireHttpsUrl(manifestInterface.websiteURL, manifestRelPath, 'interface.websiteURL', result);
|
|
323
|
+
requireHttpsUrl(manifestInterface.privacyPolicyURL, manifestRelPath, 'interface.privacyPolicyURL', result);
|
|
324
|
+
requireHttpsUrl(manifestInterface.termsOfServiceURL, manifestRelPath, 'interface.termsOfServiceURL', result);
|
|
325
|
+
assertRelativeAsset(repoRoot, pluginRoot, manifestInterface.composerIcon, manifestRelPath, 'interface.composerIcon', result);
|
|
326
|
+
assertRelativeAsset(repoRoot, pluginRoot, manifestInterface.logo, manifestRelPath, 'interface.logo', result);
|
|
327
|
+
const prompts = asArray(manifestInterface.defaultPrompt);
|
|
328
|
+
if (prompts.length === 0 || prompts.length > 3) {
|
|
329
|
+
addIssue(result.errors, manifestRelPath, 'interface.defaultPrompt must contain one to three prompts');
|
|
330
|
+
}
|
|
331
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
332
|
+
if (!stringValue(prompt) || String(prompt).length > 128) {
|
|
333
|
+
addIssue(result.errors, manifestRelPath, `interface.defaultPrompt[${index}] must be a non-empty string of at most 128 characters`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const [index, screenshot] of asArray(manifestInterface.screenshots).entries()) {
|
|
337
|
+
assertRelativeAsset(repoRoot, pluginRoot, screenshot, manifestRelPath, `interface.screenshots[${index}]`, result);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath, productVersion, result) {
|
|
341
|
+
if (!manifest)
|
|
342
|
+
return;
|
|
343
|
+
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
344
|
+
assertEqual(manifest.version, productVersion, manifestRelPath, 'version', result);
|
|
345
|
+
assertEqual(manifest.skills, './skills/', manifestRelPath, 'skills', result);
|
|
346
|
+
assertEqual(manifest.mcpServers, './.mcp.json', manifestRelPath, 'mcpServers', result);
|
|
347
|
+
const manifestInterface = asObject(manifest.interface);
|
|
348
|
+
if (!manifestInterface) {
|
|
349
|
+
addIssue(result.errors, manifestRelPath, 'interface must be an object');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
validateCodexManifestInterface(repoRoot, pluginRoot, manifestRelPath, manifestInterface, result);
|
|
353
|
+
}
|
|
354
|
+
function validateCodexMcpConfig(mcpConfig, mcpRelPath, remoteMcpUrl, result) {
|
|
355
|
+
if (!mcpConfig)
|
|
356
|
+
return;
|
|
357
|
+
const servers = asObject(mcpConfig.mcpServers);
|
|
358
|
+
const fraimServer = asObject(servers?.fraim);
|
|
359
|
+
if (!fraimServer) {
|
|
360
|
+
addIssue(result.errors, mcpRelPath, 'mcpServers.fraim must be configured');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
assertEqual(fraimServer.type, 'http', mcpRelPath, 'mcpServers.fraim.type', result);
|
|
364
|
+
assertEqual(fraimServer.url, remoteMcpUrl, mcpRelPath, 'mcpServers.fraim.url', result);
|
|
365
|
+
if ('headers' in fraimServer || 'env' in fraimServer) {
|
|
366
|
+
addIssue(result.errors, mcpRelPath, 'mcpServers.fraim must not embed headers or env credentials');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function validateCodexSkill(repoRoot, skillRelPath, result) {
|
|
370
|
+
assertFile(repoRoot, skillRelPath, result);
|
|
371
|
+
const skillPath = resolveRepoPath(repoRoot, skillRelPath);
|
|
372
|
+
if (!(0, fs_1.existsSync)(skillPath))
|
|
373
|
+
return;
|
|
374
|
+
const skillContent = (0, fs_1.readFileSync)(skillPath, 'utf8');
|
|
375
|
+
if (!/^---\r?\n/.test(skillContent)) {
|
|
376
|
+
addIssue(result.errors, skillRelPath, 'skill must start with YAML frontmatter');
|
|
377
|
+
}
|
|
378
|
+
if (!skillContent.includes('seekMentoring')) {
|
|
379
|
+
addIssue(result.errors, skillRelPath, 'skill should preserve FRAIM phase mentoring guidance');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function validateFrontmatterMarkdown(repoRoot, relPath, result) {
|
|
383
|
+
assertFile(repoRoot, relPath, result);
|
|
384
|
+
const filePath = resolveRepoPath(repoRoot, relPath);
|
|
385
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
386
|
+
return;
|
|
387
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
388
|
+
if (!/^---\r?\n/.test(content)) {
|
|
389
|
+
addIssue(result.errors, relPath, 'markdown file must start with YAML frontmatter');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function validateCodexPackage(repoRoot, result, targetDetails) {
|
|
393
|
+
const codexRoot = path_1.default.join(TARGET_ROOT, 'codex');
|
|
394
|
+
const marketplaceRelPath = path_1.default.join(codexRoot, '.agents', 'plugins', 'marketplace.json');
|
|
395
|
+
const pluginRoot = path_1.default.join(codexRoot, 'plugins', 'fraim');
|
|
396
|
+
const manifestRelPath = path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json');
|
|
397
|
+
const mcpRelPath = path_1.default.join(pluginRoot, '.mcp.json');
|
|
398
|
+
const skillRelPath = path_1.default.join(pluginRoot, 'skills', 'fraim', 'SKILL.md');
|
|
399
|
+
const marketplace = readJsonObject(repoRoot, marketplaceRelPath, result);
|
|
400
|
+
const manifest = readJsonObject(repoRoot, manifestRelPath, result);
|
|
401
|
+
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
402
|
+
validateCodexMarketplace(marketplace, marketplaceRelPath, result);
|
|
403
|
+
validateCodexManifest(repoRoot, pluginRoot, manifest, manifestRelPath, targetDetails.productVersion, result);
|
|
404
|
+
validateCodexMcpConfig(mcpConfig, mcpRelPath, targetDetails.remoteMcpUrl, result);
|
|
405
|
+
validateCodexSkill(repoRoot, skillRelPath, result);
|
|
406
|
+
}
|
|
407
|
+
function validateSimpleMarketplaceManifest(marketplace, marketplaceRelPath, expectedName, expectedSource, productVersion, result) {
|
|
408
|
+
if (!marketplace)
|
|
409
|
+
return;
|
|
410
|
+
assertEqual(marketplace.name, expectedName, marketplaceRelPath, 'name', result);
|
|
411
|
+
if (!asObject(marketplace.owner)) {
|
|
412
|
+
addIssue(result.errors, marketplaceRelPath, 'owner must be an object');
|
|
413
|
+
}
|
|
414
|
+
const entries = asArray(marketplace.plugins);
|
|
415
|
+
const fraimEntry = entries
|
|
416
|
+
.map((entry) => asObject(entry))
|
|
417
|
+
.find((entry) => entry?.name === 'fraim');
|
|
418
|
+
if (!fraimEntry) {
|
|
419
|
+
addIssue(result.errors, marketplaceRelPath, 'plugins must include fraim');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
assertEqual(fraimEntry.source, expectedSource, marketplaceRelPath, 'fraim.source', result);
|
|
423
|
+
assertEqual(fraimEntry.version, productVersion, marketplaceRelPath, 'fraim.version', result);
|
|
424
|
+
}
|
|
425
|
+
function validateMcpRegistryPackage(repoRoot, result, targetDetails) {
|
|
426
|
+
const registryRoot = path_1.default.join(TARGET_ROOT, 'mcp-registry');
|
|
427
|
+
const serverRelPath = path_1.default.join(registryRoot, 'server.json');
|
|
428
|
+
const serverJson = readJsonObject(repoRoot, serverRelPath, result);
|
|
429
|
+
assertFile(repoRoot, path_1.default.join(registryRoot, 'README.md'), result);
|
|
430
|
+
if (!serverJson)
|
|
431
|
+
return;
|
|
432
|
+
requireHttpsUrl(serverJson.$schema, serverRelPath, '$schema', result);
|
|
433
|
+
assertEqual(serverJson.name, 'io.github.mathursrus/fraim', serverRelPath, 'name', result);
|
|
434
|
+
assertEqual(serverJson.title, 'FRAIM', serverRelPath, 'title', result);
|
|
435
|
+
assertEqual(serverJson.version, targetDetails.productVersion, serverRelPath, 'version', result);
|
|
436
|
+
requireHttpsUrl(serverJson.websiteUrl, serverRelPath, 'websiteUrl', result);
|
|
437
|
+
const repository = asObject(serverJson.repository);
|
|
438
|
+
assertEqual(repository?.url, 'https://github.com/mathursrus/FRAIM', serverRelPath, 'repository.url', result);
|
|
439
|
+
assertEqual(repository?.source, 'github', serverRelPath, 'repository.source', result);
|
|
440
|
+
const remotes = asArray(serverJson.remotes);
|
|
441
|
+
const fraimRemote = remotes.map((remote) => asObject(remote)).find((remote) => remote?.url === targetDetails.remoteMcpUrl);
|
|
442
|
+
if (!fraimRemote) {
|
|
443
|
+
addIssue(result.errors, serverRelPath, 'remotes must include the FRAIM remote MCP URL');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
assertEqual(fraimRemote.type, 'streamable-http', serverRelPath, 'remotes.fraim.type', result);
|
|
447
|
+
if ('headers' in fraimRemote) {
|
|
448
|
+
addIssue(result.errors, serverRelPath, 'remotes.fraim must not embed headers');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function validateCursorPackage(repoRoot, result, targetDetails) {
|
|
452
|
+
const cursorRoot = path_1.default.join(TARGET_ROOT, 'cursor');
|
|
453
|
+
const targetMarketplaceRelPath = path_1.default.join(cursorRoot, '.cursor-plugin', 'marketplace.json');
|
|
454
|
+
const pluginRoot = path_1.default.join(cursorRoot, 'plugins', 'fraim');
|
|
455
|
+
const manifestRelPath = path_1.default.join(pluginRoot, '.cursor-plugin', 'plugin.json');
|
|
456
|
+
const mcpRelPath = path_1.default.join(pluginRoot, 'mcp.json');
|
|
457
|
+
const ruleRelPath = path_1.default.join(pluginRoot, 'rules', 'fraim.mdc');
|
|
458
|
+
const skillRelPath = path_1.default.join(pluginRoot, 'skills', 'fraim', 'SKILL.md');
|
|
459
|
+
const commandRelPath = path_1.default.join(pluginRoot, 'commands', 'fraim.md');
|
|
460
|
+
const rootMarketplace = readJsonObject(repoRoot, ROOT_CURSOR_MARKETPLACE_PATH, result);
|
|
461
|
+
const targetMarketplace = readJsonObject(repoRoot, targetMarketplaceRelPath, result);
|
|
462
|
+
const manifest = readJsonObject(repoRoot, manifestRelPath, result);
|
|
463
|
+
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
464
|
+
assertFile(repoRoot, path_1.default.join(cursorRoot, 'README.md'), result);
|
|
465
|
+
assertFile(repoRoot, path_1.default.join(pluginRoot, 'README.md'), result);
|
|
466
|
+
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_CURSOR_MARKETPLACE_PATH, 'fraim-marketplace', 'marketplaces/fraim/cursor/plugins/fraim', targetDetails.productVersion, result);
|
|
467
|
+
validateSimpleMarketplaceManifest(targetMarketplace, targetMarketplaceRelPath, 'fraim-marketplace', 'plugins/fraim', targetDetails.productVersion, result);
|
|
468
|
+
if (manifest) {
|
|
469
|
+
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
470
|
+
assertEqual(manifest.version, targetDetails.productVersion, manifestRelPath, 'version', result);
|
|
471
|
+
assertEqual(manifest.rules, 'rules/', manifestRelPath, 'rules', result);
|
|
472
|
+
assertEqual(manifest.skills, 'skills/', manifestRelPath, 'skills', result);
|
|
473
|
+
assertEqual(manifest.commands, 'commands/', manifestRelPath, 'commands', result);
|
|
474
|
+
assertEqual(manifest.mcpServers, 'mcp.json', manifestRelPath, 'mcpServers', result);
|
|
475
|
+
assertRelativeAsset(repoRoot, pluginRoot, manifest.logo, manifestRelPath, 'logo', result);
|
|
476
|
+
}
|
|
477
|
+
if (mcpConfig) {
|
|
478
|
+
const servers = asObject(mcpConfig.mcpServers);
|
|
479
|
+
const fraimServer = asObject(servers?.fraim);
|
|
480
|
+
if (!fraimServer) {
|
|
481
|
+
addIssue(result.errors, mcpRelPath, 'mcpServers.fraim must be configured');
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
assertEqual(fraimServer.url, targetDetails.remoteMcpUrl, mcpRelPath, 'mcpServers.fraim.url', result);
|
|
485
|
+
if ('headers' in fraimServer || 'env' in fraimServer || 'auth' in fraimServer) {
|
|
486
|
+
addIssue(result.errors, mcpRelPath, 'mcpServers.fraim must not embed credentials');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
assertFile(repoRoot, ruleRelPath, result);
|
|
491
|
+
const rulePath = resolveRepoPath(repoRoot, ruleRelPath);
|
|
492
|
+
if ((0, fs_1.existsSync)(rulePath)) {
|
|
493
|
+
const ruleContent = (0, fs_1.readFileSync)(rulePath, 'utf8');
|
|
494
|
+
if (!/^---\r?\n/.test(ruleContent) || !ruleContent.includes('alwaysApply: true')) {
|
|
495
|
+
addIssue(result.errors, ruleRelPath, 'Cursor rule must include alwaysApply frontmatter');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
validateCodexSkill(repoRoot, skillRelPath, result);
|
|
499
|
+
validateFrontmatterMarkdown(repoRoot, commandRelPath, result);
|
|
500
|
+
}
|
|
501
|
+
function validateCopilotPackage(repoRoot, result, targetDetails) {
|
|
502
|
+
const vscodeRoot = path_1.default.join(TARGET_ROOT, 'vscode');
|
|
503
|
+
const pluginRoot = path_1.default.join(vscodeRoot, 'plugins', 'fraim');
|
|
504
|
+
const manifestRelPath = path_1.default.join(pluginRoot, 'plugin.json');
|
|
505
|
+
const mcpRelPath = path_1.default.join(pluginRoot, '.mcp.json');
|
|
506
|
+
const skillRelPath = path_1.default.join(pluginRoot, 'skills', 'fraim', 'SKILL.md');
|
|
507
|
+
const commandRelPath = path_1.default.join(pluginRoot, 'commands', 'fraim.md');
|
|
508
|
+
const rootMarketplace = readJsonObject(repoRoot, ROOT_COPILOT_MARKETPLACE_PATH, result);
|
|
509
|
+
const manifest = readJsonObject(repoRoot, manifestRelPath, result);
|
|
510
|
+
const mcpConfig = readJsonObject(repoRoot, mcpRelPath, result);
|
|
511
|
+
assertFile(repoRoot, path_1.default.join(vscodeRoot, 'README.md'), result);
|
|
512
|
+
assertFile(repoRoot, path_1.default.join(pluginRoot, 'README.md'), result);
|
|
513
|
+
validateSimpleMarketplaceManifest(rootMarketplace, ROOT_COPILOT_MARKETPLACE_PATH, 'fraim-copilot-marketplace', 'marketplaces/fraim/vscode/plugins/fraim', targetDetails.productVersion, result);
|
|
514
|
+
if (manifest) {
|
|
515
|
+
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
516
|
+
assertEqual(manifest.version, targetDetails.productVersion, manifestRelPath, 'version', result);
|
|
517
|
+
assertEqual(manifest.skills, 'skills/', manifestRelPath, 'skills', result);
|
|
518
|
+
assertEqual(manifest.commands, 'commands/', manifestRelPath, 'commands', result);
|
|
519
|
+
assertEqual(manifest.mcpServers, '.mcp.json', manifestRelPath, 'mcpServers', result);
|
|
520
|
+
}
|
|
521
|
+
validateCodexMcpConfig(mcpConfig, mcpRelPath, targetDetails.remoteMcpUrl, result);
|
|
522
|
+
validateCodexSkill(repoRoot, skillRelPath, result);
|
|
523
|
+
validateFrontmatterMarkdown(repoRoot, commandRelPath, result);
|
|
524
|
+
}
|
|
525
|
+
function validateGeminiPackage(repoRoot, result, targetDetails) {
|
|
526
|
+
const geminiRoot = path_1.default.join(TARGET_ROOT, 'gemini');
|
|
527
|
+
const extensionRoot = path_1.default.join(geminiRoot, 'extension');
|
|
528
|
+
const manifestRelPath = path_1.default.join(extensionRoot, 'gemini-extension.json');
|
|
529
|
+
const commandRelPath = path_1.default.join(extensionRoot, 'commands', 'fraim.toml');
|
|
530
|
+
const skillRelPath = path_1.default.join(extensionRoot, 'skills', 'fraim', 'SKILL.md');
|
|
531
|
+
const manifest = readJsonObject(repoRoot, manifestRelPath, result);
|
|
532
|
+
assertFile(repoRoot, path_1.default.join(geminiRoot, 'README.md'), result);
|
|
533
|
+
assertFile(repoRoot, path_1.default.join(extensionRoot, 'GEMINI.md'), result);
|
|
534
|
+
assertFile(repoRoot, commandRelPath, result);
|
|
535
|
+
validateCodexSkill(repoRoot, skillRelPath, result);
|
|
536
|
+
if (!manifest)
|
|
537
|
+
return;
|
|
538
|
+
assertEqual(manifest.name, 'fraim', manifestRelPath, 'name', result);
|
|
539
|
+
assertEqual(manifest.version, targetDetails.productVersion, manifestRelPath, 'version', result);
|
|
540
|
+
assertEqual(manifest.contextFileName, 'GEMINI.md', manifestRelPath, 'contextFileName', result);
|
|
541
|
+
const servers = asObject(manifest.mcpServers);
|
|
542
|
+
const fraimServer = asObject(servers?.fraim);
|
|
543
|
+
if (!fraimServer) {
|
|
544
|
+
addIssue(result.errors, manifestRelPath, 'mcpServers.fraim must be configured');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
assertEqual(fraimServer.httpUrl, targetDetails.remoteMcpUrl, manifestRelPath, 'mcpServers.fraim.httpUrl', result);
|
|
548
|
+
if ('headers' in fraimServer || 'env' in fraimServer) {
|
|
549
|
+
addIssue(result.errors, manifestRelPath, 'mcpServers.fraim must not embed credentials');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function validateMarketplaceBundles(repoRoot = process.cwd()) {
|
|
553
|
+
const result = makeResult();
|
|
554
|
+
assertFile(repoRoot, path_1.default.join(TARGET_ROOT, 'README.md'), result);
|
|
555
|
+
assertFile(repoRoot, ACCEPTANCE_EVIDENCE_PATH, result);
|
|
556
|
+
const targetDetails = validateTargetMatrix(repoRoot, result);
|
|
557
|
+
validateOpenAiPacket(repoRoot, result, targetDetails);
|
|
558
|
+
validatePolicyPages(repoRoot, result);
|
|
559
|
+
validateCodexPackage(repoRoot, result, targetDetails);
|
|
560
|
+
validateMcpRegistryPackage(repoRoot, result, targetDetails);
|
|
561
|
+
validateCursorPackage(repoRoot, result, targetDetails);
|
|
562
|
+
validateCopilotPackage(repoRoot, result, targetDetails);
|
|
563
|
+
validateGeminiPackage(repoRoot, result, targetDetails);
|
|
564
|
+
scanForSecrets(repoRoot, TARGET_ROOT, result);
|
|
565
|
+
scanForSecrets(repoRoot, ROOT_CURSOR_MARKETPLACE_PATH, result);
|
|
566
|
+
scanForSecrets(repoRoot, ROOT_COPILOT_MARKETPLACE_PATH, result);
|
|
567
|
+
scanForSecrets(repoRoot, ACCEPTANCE_EVIDENCE_PATH, result);
|
|
568
|
+
for (const policyPagePath of POLICY_PAGE_PATHS) {
|
|
569
|
+
scanForSecrets(repoRoot, policyPagePath, result);
|
|
570
|
+
}
|
|
571
|
+
result.checkedFiles.sort();
|
|
572
|
+
result.errors.sort((a, b) => `${a.path}:${a.message}`.localeCompare(`${b.path}:${b.message}`));
|
|
573
|
+
result.warnings.sort((a, b) => `${a.path}:${a.message}`.localeCompare(`${b.path}:${b.message}`));
|
|
574
|
+
result.ok = result.errors.length === 0;
|
|
575
|
+
return result;
|
|
576
|
+
}
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -44,6 +44,7 @@ const list_1 = require("./commands/list");
|
|
|
44
44
|
const setup_1 = require("./commands/setup");
|
|
45
45
|
const init_project_1 = require("./commands/init-project");
|
|
46
46
|
const add_ide_1 = require("./commands/add-ide");
|
|
47
|
+
const add_surface_1 = require("./commands/add-surface");
|
|
47
48
|
const add_provider_1 = require("./commands/add-provider");
|
|
48
49
|
const override_1 = require("./commands/override");
|
|
49
50
|
const list_overridable_1 = require("./commands/list-overridable");
|
|
@@ -89,6 +90,7 @@ program.addCommand(list_1.listCommand);
|
|
|
89
90
|
program.addCommand(setup_1.setupCommand);
|
|
90
91
|
program.addCommand(init_project_1.initProjectCommand);
|
|
91
92
|
program.addCommand(add_ide_1.addIDECommand);
|
|
93
|
+
program.addCommand(add_surface_1.addSurfaceCommand);
|
|
92
94
|
program.addCommand(add_provider_1.addProviderCommand);
|
|
93
95
|
program.addCommand(override_1.overrideCommand);
|
|
94
96
|
program.addCommand(list_overridable_1.listOverridableCommand);
|
|
@@ -219,12 +219,14 @@ class CodexFormat {
|
|
|
219
219
|
for (const [key, server] of servers) {
|
|
220
220
|
if (server.url) {
|
|
221
221
|
// HTTP server
|
|
222
|
-
const
|
|
223
|
-
const escapedToken = this.escapeToml(token);
|
|
222
|
+
const authHeader = server.headers?.Authorization;
|
|
224
223
|
const escapedUrl = this.escapeToml(server.url);
|
|
225
224
|
sections.push(`[mcp_servers.${key}]`);
|
|
226
225
|
sections.push(`url = "${escapedUrl}"`);
|
|
227
|
-
|
|
226
|
+
// OAuth-first providers (e.g. GitHub) have no Authorization header — IDE manages auth
|
|
227
|
+
if (authHeader) {
|
|
228
|
+
sections.push(`http_headers = { Authorization = "${this.escapeToml(authHeader)}" }`);
|
|
229
|
+
}
|
|
228
230
|
sections.push('');
|
|
229
231
|
}
|
|
230
232
|
else {
|
|
@@ -81,18 +81,25 @@ async function buildProviderMCPServer(providerId, token, config) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* Build an HTTP MCP server
|
|
84
|
+
* Build an HTTP MCP server.
|
|
85
|
+
* When authHeaderTemplate is absent the provider uses IDE-native OAuth (e.g. GitHub);
|
|
86
|
+
* write a URL-only entry so the IDE handles the OAuth flow on first tool use.
|
|
85
87
|
*/
|
|
86
88
|
function buildHTTPServer(mcpConfig, token) {
|
|
87
89
|
if (!mcpConfig.url) {
|
|
88
90
|
throw new Error('HTTP MCP server requires url');
|
|
89
91
|
}
|
|
90
|
-
|
|
92
|
+
if (!mcpConfig.authHeaderTemplate) {
|
|
93
|
+
return {
|
|
94
|
+
type: 'http',
|
|
95
|
+
url: mcpConfig.url
|
|
96
|
+
};
|
|
97
|
+
}
|
|
91
98
|
return {
|
|
92
99
|
type: 'http',
|
|
93
100
|
url: mcpConfig.url,
|
|
94
101
|
headers: {
|
|
95
|
-
Authorization:
|
|
102
|
+
Authorization: mcpConfig.authHeaderTemplate.replace('{token}', token)
|
|
96
103
|
}
|
|
97
104
|
};
|
|
98
105
|
}
|
|
@@ -22,12 +22,11 @@ const LOCAL_PROVIDERS = [
|
|
|
22
22
|
description: 'GitHub repository and issue management',
|
|
23
23
|
capabilities: ['code', 'issues', 'integrated'],
|
|
24
24
|
docsUrl: 'https://docs.github.com',
|
|
25
|
-
setupInstructions: '
|
|
25
|
+
setupInstructions: 'Run "fraim add-provider github" — your IDE handles OAuth automatically on first use',
|
|
26
26
|
hasAdditionalConfig: false,
|
|
27
27
|
mcpServer: {
|
|
28
28
|
type: 'http',
|
|
29
|
-
url: 'https://api.githubcopilot.com/mcp/'
|
|
30
|
-
authHeaderTemplate: 'Bearer {token}'
|
|
29
|
+
url: 'https://api.githubcopilot.com/mcp/'
|
|
31
30
|
}
|
|
32
31
|
},
|
|
33
32
|
{
|