fraim 2.0.276 → 2.0.278
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/cli/utils/pack-home.js +40 -0
- package/dist/src/core/job-visualization.js +161 -0
- package/dist/src/core/utils/job-parser.js +6 -3
- package/dist/src/core/utils/local-registry-resolver.js +1 -0
- package/dist/src/local-mcp-server/stdio-server.js +5 -13
- package/dist/src/mcp/tool-schemas.js +376 -0
- package/dist/src/services/mcp-service.js +1 -5
- package/dist/src/services/registry-service.js +15 -11
- package/package.json +4 -3
|
@@ -7,6 +7,7 @@ exports.packsDir = packsDir;
|
|
|
7
7
|
exports.defaultCloneDir = defaultCloneDir;
|
|
8
8
|
exports.resolvePackHome = resolvePackHome;
|
|
9
9
|
exports.packReadRoots = packReadRoots;
|
|
10
|
+
exports.personalizedCapabilityReadRoots = personalizedCapabilityReadRoots;
|
|
10
11
|
exports.migrateStrandedContent = migrateStrandedContent;
|
|
11
12
|
exports.findStrandedLegacyContent = findStrandedLegacyContent;
|
|
12
13
|
exports.gitUrlHasUserinfo = gitUrlHasUserinfo;
|
|
@@ -173,6 +174,45 @@ function resolvePackHome(layer) {
|
|
|
173
174
|
function packReadRoots(layer) {
|
|
174
175
|
return [resolvePackHome(layer).contentRoot];
|
|
175
176
|
}
|
|
177
|
+
function displayPathForContentRoot(contentRoot) {
|
|
178
|
+
const userRoot = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
179
|
+
const rel = path_1.default.relative(userRoot, contentRoot);
|
|
180
|
+
if (rel === '' || (rel && !rel.startsWith('..') && !path_1.default.isAbsolute(rel))) {
|
|
181
|
+
return (0, project_fraim_paths_1.getUserFraimDisplayPath)(rel);
|
|
182
|
+
}
|
|
183
|
+
return contentRoot.replace(/\\/g, '/');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Return personalized capability roots in merge order: org, manager, project.
|
|
187
|
+
*
|
|
188
|
+
* Callers that de-duplicate by key can iterate this array directly and let
|
|
189
|
+
* later entries overwrite earlier ones, matching capability resolution
|
|
190
|
+
* precedence where project overrides manager and manager overrides org.
|
|
191
|
+
*/
|
|
192
|
+
function personalizedCapabilityReadRoots(projectRoot, capabilityDir) {
|
|
193
|
+
const roots = [];
|
|
194
|
+
for (const layer of ['org', 'manager']) {
|
|
195
|
+
const contentRoot = resolvePackHome(layer).contentRoot;
|
|
196
|
+
roots.push({
|
|
197
|
+
scope: layer,
|
|
198
|
+
contentRoot,
|
|
199
|
+
capabilityRoot: path_1.default.join(contentRoot, capabilityDir),
|
|
200
|
+
displayRoot: contentRoot,
|
|
201
|
+
displayPrefix: displayPathForContentRoot(contentRoot),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (projectRoot) {
|
|
205
|
+
const contentRoot = (0, project_fraim_paths_1.getWorkspaceFraimPath)(projectRoot, 'personalized-employee');
|
|
206
|
+
roots.push({
|
|
207
|
+
scope: 'project',
|
|
208
|
+
contentRoot,
|
|
209
|
+
capabilityRoot: path_1.default.join(contentRoot, capabilityDir),
|
|
210
|
+
displayRoot: contentRoot,
|
|
211
|
+
displayPrefix: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee'),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return roots;
|
|
215
|
+
}
|
|
176
216
|
/**
|
|
177
217
|
* Auto-migrate content stranded at the legacy standard path to the configured
|
|
178
218
|
* contentRoot. Runs at most once per process per (layer + contentRoot) pair.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildJobVisualization = buildJobVisualization;
|
|
4
|
+
const local_registry_resolver_1 = require("./utils/local-registry-resolver");
|
|
5
|
+
const job_parser_1 = require("./utils/job-parser");
|
|
6
|
+
function friendlyLabel(id) {
|
|
7
|
+
const PREFIXES = ['implement-', 'address-', 'spec-', 'design-', 'context-'];
|
|
8
|
+
let body = id;
|
|
9
|
+
for (const p of PREFIXES) {
|
|
10
|
+
if (body.startsWith(p) && body.length > p.length) {
|
|
11
|
+
body = body.slice(p.length);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const spaced = body.replace(/-/g, ' ').trim();
|
|
16
|
+
if (!spaced)
|
|
17
|
+
return id;
|
|
18
|
+
return spaced[0].toUpperCase() + spaced.slice(1).toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
function readSection(content, heading) {
|
|
21
|
+
const re = new RegExp(`(?:^|\\r?\\n)#{1,6} ${heading}[ \\t]*\\r?\\n([\\s\\S]*?)(?=\\r?\\n#{1,6} |\\r?\\n---|$)`);
|
|
22
|
+
const m = content.match(re);
|
|
23
|
+
if (!m)
|
|
24
|
+
return null;
|
|
25
|
+
const text = m[1]
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((l) => l.trim())
|
|
28
|
+
.filter((l) => l && !l.startsWith('{{'))
|
|
29
|
+
.map((l) => l.replace(/^[-*]\s*/, ''))
|
|
30
|
+
.join(' ')
|
|
31
|
+
.trim();
|
|
32
|
+
return text || null;
|
|
33
|
+
}
|
|
34
|
+
function parseSkillIdFromInclude(line) {
|
|
35
|
+
const m = line.match(/\{\{include:skills\/([^}]+?)(?:\.md)?\}\}/);
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
return m[1].replace(/\.md$/, '');
|
|
39
|
+
}
|
|
40
|
+
function skillLabelFromId(skillId) {
|
|
41
|
+
const base = skillId.includes('/') ? skillId.split('/').pop() : skillId;
|
|
42
|
+
return base.replace(/-/g, ' ');
|
|
43
|
+
}
|
|
44
|
+
function isStub(content) {
|
|
45
|
+
return (content.includes('<!-- FRAIM_DISCOVERY_STUB -->') ||
|
|
46
|
+
content.includes('STUB:') ||
|
|
47
|
+
content.includes('<!-- STUB -->'));
|
|
48
|
+
}
|
|
49
|
+
function buildResolver(projectPath) {
|
|
50
|
+
// remoteContentResolver is required by LocalRegistryResolver but never reached
|
|
51
|
+
// for local repos: findRegistryPath now searches registry/ as a fallback layer,
|
|
52
|
+
// and readWorkspaceRegistryFile handles the direct path lookup.
|
|
53
|
+
return new local_registry_resolver_1.LocalRegistryResolver({
|
|
54
|
+
workspaceRoot: projectPath,
|
|
55
|
+
shouldFilter: isStub,
|
|
56
|
+
remoteContentResolver: async (registryPath) => {
|
|
57
|
+
throw new Error(`registry file not found locally: ${registryPath}`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function toTitleCase(id) {
|
|
62
|
+
return id
|
|
63
|
+
.split('-')
|
|
64
|
+
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
|
|
65
|
+
.join(' ');
|
|
66
|
+
}
|
|
67
|
+
function parseJobTitle(content, jobId) {
|
|
68
|
+
const m = content.match(/^#\s+(?:FRAIM Job:\s*)?(.+)/m);
|
|
69
|
+
if (m) {
|
|
70
|
+
const raw = m[1].trim();
|
|
71
|
+
if (/^[a-z0-9-]+$/.test(raw))
|
|
72
|
+
return toTitleCase(raw);
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
return toTitleCase(jobId);
|
|
76
|
+
}
|
|
77
|
+
function parseJobLevelIntent(content) {
|
|
78
|
+
const m = content.match(/(?:^|\r?\n)## Intent[ \t]*\r?\n([\s\S]*?)(?=\r?\n#{1,6} |\r?\n---|$)/);
|
|
79
|
+
if (!m)
|
|
80
|
+
return null;
|
|
81
|
+
const lines = m[1].split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('{{'));
|
|
82
|
+
const text = lines.map((l) => l.replace(/^[-*]\s*/, '')).join(' ').trim();
|
|
83
|
+
return text || null;
|
|
84
|
+
}
|
|
85
|
+
const EMPTY_RESPONSE = (jobId) => ({
|
|
86
|
+
jobId,
|
|
87
|
+
title: toTitleCase(jobId),
|
|
88
|
+
intent: null,
|
|
89
|
+
personalized: false,
|
|
90
|
+
phases: [],
|
|
91
|
+
});
|
|
92
|
+
async function buildJobVisualization(jobId, projectPath) {
|
|
93
|
+
const resolver = buildResolver(projectPath);
|
|
94
|
+
const registryPath = await resolver.findRegistryPath('jobs', jobId);
|
|
95
|
+
let jobFile;
|
|
96
|
+
try {
|
|
97
|
+
jobFile = await resolver.resolveFile(registryPath, {
|
|
98
|
+
includeMetadata: false,
|
|
99
|
+
stripMcpHeader: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return EMPTY_RESPONSE(jobId);
|
|
104
|
+
}
|
|
105
|
+
const content = jobFile.content;
|
|
106
|
+
if (!content?.trim())
|
|
107
|
+
return EMPTY_RESPONSE(jobId);
|
|
108
|
+
const title = parseJobTitle(content, jobId);
|
|
109
|
+
const intent = parseJobLevelIntent(content);
|
|
110
|
+
// Phase splitting reuses JobParser — the same parser the MCP proxy uses.
|
|
111
|
+
const jobDef = job_parser_1.JobParser.parseContent(content, jobId);
|
|
112
|
+
if (!jobDef || jobDef.phases.size === 0) {
|
|
113
|
+
return { jobId, title, intent, personalized: jobFile.personalized, scope: jobFile.scope, phases: [] };
|
|
114
|
+
}
|
|
115
|
+
const phases = await Promise.all([...jobDef.phases.entries()].map(async ([phaseId, phaseContent]) => {
|
|
116
|
+
const phaseIntent = readSection(phaseContent, 'Intent');
|
|
117
|
+
const phaseOutcome = readSection(phaseContent, 'Outcome');
|
|
118
|
+
const skillIds = [];
|
|
119
|
+
for (const line of phaseContent.split(/\r?\n/)) {
|
|
120
|
+
const id = parseSkillIdFromInclude(line);
|
|
121
|
+
if (id)
|
|
122
|
+
skillIds.push(id);
|
|
123
|
+
}
|
|
124
|
+
const skills = await Promise.all(skillIds.map(async (skillId) => {
|
|
125
|
+
try {
|
|
126
|
+
const skillFile = await resolver.resolveFile(`skills/${skillId}.md`, {
|
|
127
|
+
includeMetadata: false,
|
|
128
|
+
stripMcpHeader: true,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
id: skillId,
|
|
132
|
+
label: skillLabelFromId(skillId),
|
|
133
|
+
input: readSection(skillFile.content, 'Skill Input'),
|
|
134
|
+
output: readSection(skillFile.content, 'Skill Output'),
|
|
135
|
+
personalized: skillFile.personalized,
|
|
136
|
+
scope: skillFile.scope,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { id: skillId, label: skillLabelFromId(skillId), input: null, output: null, personalized: false };
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
return {
|
|
144
|
+
id: phaseId,
|
|
145
|
+
label: friendlyLabel(phaseId),
|
|
146
|
+
intent: phaseIntent,
|
|
147
|
+
outcome: phaseOutcome,
|
|
148
|
+
personalized: jobFile.personalized,
|
|
149
|
+
scope: jobFile.scope,
|
|
150
|
+
skills,
|
|
151
|
+
};
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
jobId,
|
|
155
|
+
title,
|
|
156
|
+
intent,
|
|
157
|
+
personalized: jobFile.personalized,
|
|
158
|
+
scope: jobFile.scope,
|
|
159
|
+
phases,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -130,9 +130,12 @@ class JobParser {
|
|
|
130
130
|
}
|
|
131
131
|
static parseSimpleJob(filePath, content) {
|
|
132
132
|
const jobName = (0, path_1.basename)(filePath, '.md');
|
|
133
|
-
const metadata = {
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
const metadata = { name: jobName };
|
|
134
|
+
// If the content contains ## Phase: sections, parse them the same way as
|
|
135
|
+
// phase-based jobs so callers always get a populated phases Map.
|
|
136
|
+
if (/^##\s+Phase:/im.test(content)) {
|
|
137
|
+
return this.parsePhaseBasedJob(filePath, content, metadata, 0);
|
|
138
|
+
}
|
|
136
139
|
return {
|
|
137
140
|
metadata,
|
|
138
141
|
overview: content.trim(),
|
|
@@ -101,6 +101,7 @@ class LocalRegistryResolver {
|
|
|
101
101
|
(0, path_1.join)(this.managerLocalRoot, dir),
|
|
102
102
|
(0, path_1.join)(this.managerCacheRoot, dir),
|
|
103
103
|
(0, path_1.join)(this.orgCacheRoot, dir),
|
|
104
|
+
(0, path_1.join)(this.workspaceRoot, 'registry', dir),
|
|
104
105
|
];
|
|
105
106
|
for (const root of searchRoots) {
|
|
106
107
|
const found = this.searchFileRecursively(root, baseName);
|
|
@@ -2272,19 +2272,9 @@ class FraimLocalMCPServer {
|
|
|
2272
2272
|
try {
|
|
2273
2273
|
const projectRoot = this.findProjectRoot();
|
|
2274
2274
|
const uniqueLocalItems = new Map();
|
|
2275
|
-
// Issue #1002 R3: list the manager's own jobs too, not only the
|
|
2276
|
-
// project's. A job authored at the manager level resolves by name
|
|
2277
|
-
// from every project, so omitting it here made it invisible in the
|
|
2278
|
-
// one place the manager looks to find out what their employee can do.
|
|
2279
2275
|
// Lowest precedence first, so a project job of the same name wins.
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
const jobDirsByLevel = [
|
|
2283
|
-
{ dir: managerJobsDir, level: 'manager' },
|
|
2284
|
-
...(projectRoot
|
|
2285
|
-
? [{ dir: (0, path_1.join)(projectRoot, 'fraim', 'personalized-employee', 'jobs'), level: 'project' }]
|
|
2286
|
-
: []),
|
|
2287
|
-
];
|
|
2276
|
+
const jobDirsByLevel = (0, pack_home_1.personalizedCapabilityReadRoots)(projectRoot, 'jobs')
|
|
2277
|
+
.map((root) => ({ dir: root.capabilityRoot, level: root.scope }));
|
|
2288
2278
|
for (const { dir: jobsDir, level } of jobDirsByLevel) {
|
|
2289
2279
|
const personalizedJobsDir = jobsDir;
|
|
2290
2280
|
if ((0, fs_1.existsSync)(personalizedJobsDir)) {
|
|
@@ -2335,7 +2325,9 @@ class FraimLocalMCPServer {
|
|
|
2335
2325
|
: '';
|
|
2336
2326
|
const levelWord = item.level === 'project'
|
|
2337
2327
|
? 'this project only'
|
|
2338
|
-
:
|
|
2328
|
+
: item.level === 'org'
|
|
2329
|
+
? 'everyone in the organization'
|
|
2330
|
+
: 'yours, every project';
|
|
2339
2331
|
combinedText += `- **${item.name}**${categorySuffix} - ${levelWord}\n`;
|
|
2340
2332
|
}
|
|
2341
2333
|
response.result.content[0].text = combinedText;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP Tool Definitions
|
|
4
|
+
*
|
|
5
|
+
* This file contains the JSON schemas for all tools exposed by the FRAIM MCP Server.
|
|
6
|
+
* Centralizing these definitions improves readability and maintainability of the McpService.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getToolDefinitions = void 0;
|
|
10
|
+
const readOnlyClosedWorldAnnotations = {
|
|
11
|
+
readOnlyHint: true,
|
|
12
|
+
openWorldHint: false,
|
|
13
|
+
destructiveHint: false,
|
|
14
|
+
};
|
|
15
|
+
const writeClosedWorldAnnotations = {
|
|
16
|
+
readOnlyHint: false,
|
|
17
|
+
openWorldHint: false,
|
|
18
|
+
destructiveHint: false,
|
|
19
|
+
};
|
|
20
|
+
const readOnlyOpenWorldAnnotations = {
|
|
21
|
+
readOnlyHint: true,
|
|
22
|
+
openWorldHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
};
|
|
25
|
+
const writeOpenWorldAnnotations = {
|
|
26
|
+
readOnlyHint: false,
|
|
27
|
+
openWorldHint: true,
|
|
28
|
+
destructiveHint: false,
|
|
29
|
+
};
|
|
30
|
+
const getToolDefinitions = (options = {}) => {
|
|
31
|
+
const surface = options.surface ?? 'local-proxy';
|
|
32
|
+
const isLocalProxySurface = surface === 'local-proxy';
|
|
33
|
+
const sessionIdProperty = isLocalProxySurface
|
|
34
|
+
? {
|
|
35
|
+
sessionId: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Active FRAIM session ID. Required on local-proxy runtime calls.'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
: {};
|
|
41
|
+
const requiredWithSession = (fields) => isLocalProxySurface
|
|
42
|
+
? ['sessionId', ...fields]
|
|
43
|
+
: fields;
|
|
44
|
+
const fraimConnectDescription = isLocalProxySurface
|
|
45
|
+
? `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
{
|
|
49
|
+
"agent": {"name": "Claude", "model": "claude-3.5-sonnet"}
|
|
50
|
+
}`
|
|
51
|
+
: `Bootstrap and initialize a FRAIM session and obtain the sessionId used by active FRAIM workflow tools. Use this after explicit FRAIM activation: the user invoked FRAIM, named a FRAIM job, asked for FRAIM job recommendations, or the active surface selected a FRAIM job. Must be called before any FRAIM workflow tool calls. For ordinary requests, do not start a FRAIM session or scan the catalog.
|
|
52
|
+
|
|
53
|
+
Hosted marketplace clients may call this with only agent information. If machine or repository context is omitted, FRAIM creates a hosted marketplace session context automatically.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
{
|
|
57
|
+
"agent": {"name": "ChatGPT", "model": "gpt-5"}
|
|
58
|
+
}`;
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
name: 'fraim_connect',
|
|
62
|
+
description: fraimConnectDescription,
|
|
63
|
+
annotations: writeClosedWorldAnnotations,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
agent: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
description: 'Agent identification',
|
|
70
|
+
properties: {
|
|
71
|
+
name: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Agent name (e.g., "Claude", "Cursor", "Kiro", "Windsurf", "Antigravity", "Grok")',
|
|
74
|
+
examples: ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity', 'Grok']
|
|
75
|
+
},
|
|
76
|
+
model: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Model name/version (e.g., "claude-3.5-sonnet", "gpt-4", "cursor-small")',
|
|
79
|
+
examples: ['claude-3.5-sonnet', 'gpt-4', 'cursor-small', 'kiro-agent']
|
|
80
|
+
},
|
|
81
|
+
version: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Agent version if available',
|
|
84
|
+
examples: ['1.0.0', '2024.12.1']
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['name', 'model'],
|
|
88
|
+
additionalProperties: true
|
|
89
|
+
},
|
|
90
|
+
...(isLocalProxySurface
|
|
91
|
+
? {}
|
|
92
|
+
: {
|
|
93
|
+
machine: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
description: 'Optional machine specifications. Hosted marketplace sessions default this when omitted.',
|
|
96
|
+
properties: {
|
|
97
|
+
hostname: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Machine hostname'
|
|
100
|
+
},
|
|
101
|
+
platform: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Platform (win32, darwin, linux)'
|
|
104
|
+
},
|
|
105
|
+
memory: {
|
|
106
|
+
type: 'number',
|
|
107
|
+
description: 'Total memory in bytes (auto-detected by local proxy)'
|
|
108
|
+
},
|
|
109
|
+
cpus: {
|
|
110
|
+
type: 'number',
|
|
111
|
+
description: 'CPU count (auto-detected by local proxy)'
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
required: ['hostname', 'platform', 'memory', 'cpus'],
|
|
115
|
+
additionalProperties: true
|
|
116
|
+
},
|
|
117
|
+
repo: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
description: 'Optional repository context. Hosted marketplace sessions default to FRAIM public catalog context when omitted.',
|
|
120
|
+
properties: {
|
|
121
|
+
url: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Git repository URL'
|
|
124
|
+
},
|
|
125
|
+
owner: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Repository owner'
|
|
128
|
+
},
|
|
129
|
+
namespace: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Repository namespace (GitLab group/subgroup path)'
|
|
132
|
+
},
|
|
133
|
+
name: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'Repository name'
|
|
136
|
+
},
|
|
137
|
+
projectPath: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Repository project path (GitLab, for example group/subgroup/repo)'
|
|
140
|
+
},
|
|
141
|
+
branch: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Current branch'
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
required: ['url'],
|
|
147
|
+
additionalProperties: true
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
issueTracking: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
description: 'Optional issue tracking context for split-provider setups (for example Jira + GitHub)',
|
|
153
|
+
properties: {
|
|
154
|
+
provider: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'Issue tracking provider',
|
|
157
|
+
enum: ['jira', 'github', 'ado', 'linear', 'gitlab']
|
|
158
|
+
},
|
|
159
|
+
owner: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: 'Issue tracking owner (GitHub)'
|
|
162
|
+
},
|
|
163
|
+
name: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
description: 'Issue tracking repository or project name'
|
|
166
|
+
},
|
|
167
|
+
organization: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Issue tracking organization (ADO)'
|
|
170
|
+
},
|
|
171
|
+
project: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Issue tracking project (ADO)'
|
|
174
|
+
},
|
|
175
|
+
namespace: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description: 'Issue tracking namespace (GitLab group/subgroup path)'
|
|
178
|
+
},
|
|
179
|
+
projectPath: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'Issue tracking project path (GitLab, for example group/subgroup/repo)'
|
|
182
|
+
},
|
|
183
|
+
baseUrl: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'Base URL for issue tracker (for example myorg.atlassian.net)'
|
|
186
|
+
},
|
|
187
|
+
projectKey: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Project key or namespace for issue IDs'
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
required: ['provider'],
|
|
193
|
+
additionalProperties: true
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
required: ['agent']
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'get_fraim_file',
|
|
201
|
+
description: `Get a specific skill, rule, or reference file from the FRAIM registry by path.
|
|
202
|
+
|
|
203
|
+
For running FRAIM jobs, use get_fraim_job instead — do NOT call get_fraim_file for job execution.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
- get_fraim_file({ path: "skills/communication/active-listening.md" })
|
|
207
|
+
- get_fraim_file({ path: "rules/local-development.md" })`,
|
|
208
|
+
annotations: readOnlyClosedWorldAnnotations,
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
...sessionIdProperty,
|
|
213
|
+
path: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: 'Path to the file (e.g., skills/communication/active-listening.md, rules/local-development.md, templates/specs/FEATURESPEC-TEMPLATE.md)'
|
|
216
|
+
},
|
|
217
|
+
raw: {
|
|
218
|
+
type: 'boolean',
|
|
219
|
+
description: 'If true, returns the raw unparsed content without any MCP headers or parsing.'
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
required: requiredWithSession(['path'])
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'get_fraim_job',
|
|
227
|
+
description: `Execute a named FRAIM job after explicit FRAIM activation. Call this tool whenever the user asks to run, start, or execute a named FRAIM job. Returns phased instructions — follow each phase and call seekMentoring to advance phases.
|
|
228
|
+
|
|
229
|
+
Do NOT use get_fraim_file to load job content. Always use get_fraim_job for job execution.
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
- get_fraim_job({ job: "feature-specification" })
|
|
233
|
+
- get_fraim_job({ job: "technical-design" })
|
|
234
|
+
- get_fraim_job({ job: "feature-implementation" })`,
|
|
235
|
+
annotations: readOnlyClosedWorldAnnotations,
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
...sessionIdProperty,
|
|
240
|
+
job: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Job name (e.g., "feature-implementation", "technical-design", "feature-specification")'
|
|
243
|
+
},
|
|
244
|
+
raw: {
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
description: 'If true, returns the raw unparsed markdown content of the job.'
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
required: requiredWithSession(['job'])
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'list_fraim_jobs',
|
|
254
|
+
description: `List the FRAIM job catalog. Use this when the user asks what FRAIM jobs are available, asks for FRAIM job recommendations, or after explicit FRAIM activation when local stubs are unavailable. Do not use this for ordinary requests. If no exact or high-confidence job match exists, do not pick the nearest job; continue normally or ask one concise clarification.`,
|
|
255
|
+
annotations: readOnlyClosedWorldAnnotations,
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
...sessionIdProperty
|
|
260
|
+
},
|
|
261
|
+
required: requiredWithSession([])
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'file_fraim_github_issue',
|
|
266
|
+
description: `Create a GitHub issue in the FRAIM repository.
|
|
267
|
+
|
|
268
|
+
Use this tool when you need to report a bug, request a feature from FRAIM. Do not use this tool to file issues in other repositories.
|
|
269
|
+
Supports dry-run mode to preview the operation.
|
|
270
|
+
|
|
271
|
+
This tool accepts text only. If visual evidence is needed, upload images to a stable shared HTTPS URL first and include those URLs in the issue body as markdown images or plain links. Do not include local file paths or base64-encoded image data in the issue body.`,
|
|
272
|
+
annotations: writeOpenWorldAnnotations,
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
...sessionIdProperty,
|
|
277
|
+
title: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
description: 'Title of the issue'
|
|
280
|
+
},
|
|
281
|
+
body: {
|
|
282
|
+
type: 'string',
|
|
283
|
+
description: 'Body/Content of the issue. If visual evidence is needed, include shared HTTPS image URLs in the body; do not include local file paths or base64 image data.'
|
|
284
|
+
},
|
|
285
|
+
labels: {
|
|
286
|
+
type: 'array',
|
|
287
|
+
items: { type: 'string' },
|
|
288
|
+
description: 'List of labels to apply'
|
|
289
|
+
},
|
|
290
|
+
dryRun: {
|
|
291
|
+
type: 'boolean',
|
|
292
|
+
description: 'If true, simulates the creation without actually notifying GitHub'
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
required: requiredWithSession(['title', 'body'])
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'list_my_fraim_github_issues',
|
|
300
|
+
description: `List issues the current FRAIM user filed into the FRAIM GitHub repository.
|
|
301
|
+
|
|
302
|
+
This tool is the read-side companion to file_fraim_github_issue. Use it when you need a simple list of issues filed by the current FRAIM user into the FRAIM repository.
|
|
303
|
+
|
|
304
|
+
Returns only:
|
|
305
|
+
- issue number
|
|
306
|
+
- title
|
|
307
|
+
- status
|
|
308
|
+
- created date
|
|
309
|
+
|
|
310
|
+
Do not use this tool for other repositories or external project issue trackers.`,
|
|
311
|
+
annotations: readOnlyOpenWorldAnnotations,
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
...sessionIdProperty,
|
|
316
|
+
limit: {
|
|
317
|
+
type: 'integer',
|
|
318
|
+
description: 'Optional maximum number of issues to return. Defaults to 20 and is capped at 100.'
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
required: requiredWithSession([])
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'seekMentoring',
|
|
326
|
+
description: `Get the instructions for the current FRAIM phase, advance to the next phase, or ask for help on the active phase after fraim_connect starts the session.`,
|
|
327
|
+
annotations: writeClosedWorldAnnotations,
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
...sessionIdProperty,
|
|
332
|
+
jobName: {
|
|
333
|
+
type: 'string',
|
|
334
|
+
description: 'Name of the job you are following',
|
|
335
|
+
},
|
|
336
|
+
jobId: {
|
|
337
|
+
type: 'string',
|
|
338
|
+
description: 'Job ID returned by get_fraim_job. Required for tracking job execution and completion.'
|
|
339
|
+
},
|
|
340
|
+
issueNumber: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
description: 'Issue number or Task ID you are working on'
|
|
343
|
+
},
|
|
344
|
+
currentPhase: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'The phase you are currently in or have just finished (e.g., "implement-scoping"). For initial workflow start, use "starting".'
|
|
347
|
+
},
|
|
348
|
+
status: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'Status of your work in the current phase',
|
|
351
|
+
enum: ['starting', 'complete', 'incomplete', 'failure']
|
|
352
|
+
},
|
|
353
|
+
findings: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
description: 'Your findings, summaries, or results from the current phase (required for status="complete")',
|
|
356
|
+
properties: {
|
|
357
|
+
uncertainties: {
|
|
358
|
+
type: 'array',
|
|
359
|
+
items: { type: 'string' },
|
|
360
|
+
description: 'Any unclear aspects that need clarification'
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
additionalProperties: true
|
|
364
|
+
},
|
|
365
|
+
evidence: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
description: 'Structured evidence or data collected (e.g., prospect counts, test results). Submit phases should put review artifacts in evidence.reviewHandoff rather than printing raw JSON to the user. Retrospective phases may put follow-on job recommendations in evidence.nextJobRecommendations (array of { jobId, label, reason?, contextSummary? }, max 3) so the work surface can offer them as next steps.',
|
|
368
|
+
additionalProperties: true
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
required: requiredWithSession(['jobName', 'jobId', 'issueNumber', 'currentPhase', 'status'])
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
};
|
|
376
|
+
exports.getToolDefinitions = getToolDefinitions;
|
|
@@ -160,7 +160,7 @@ class McpService {
|
|
|
160
160
|
? 'local-proxy'
|
|
161
161
|
: 'remote';
|
|
162
162
|
return {
|
|
163
|
-
tools: (0, tool_schemas_1.getToolDefinitions)(
|
|
163
|
+
tools: (0, tool_schemas_1.getToolDefinitions)({ surface })
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
handleListResources() {
|
|
@@ -794,9 +794,5 @@ class McpService {
|
|
|
794
794
|
throw new Error(`Unknown raw tool: ${tool}`);
|
|
795
795
|
}
|
|
796
796
|
}
|
|
797
|
-
getAvailableJobs() {
|
|
798
|
-
const jobs = Array.from(this.registryService.getFileIndex().values()).filter(f => f.type === 'job' && !f.isStub);
|
|
799
|
-
return Array.from(new Set(jobs.map(f => f.name.replace('.md', ''))));
|
|
800
|
-
}
|
|
801
797
|
}
|
|
802
798
|
exports.McpService = McpService;
|
|
@@ -9,7 +9,7 @@ const path_1 = require("path");
|
|
|
9
9
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
10
|
const stub_generator_1 = require("../core/utils/stub-generator");
|
|
11
11
|
const job_parser_1 = require("../core/utils/job-parser");
|
|
12
|
-
const
|
|
12
|
+
const pack_home_1 = require("../cli/utils/pack-home");
|
|
13
13
|
class RegistryService {
|
|
14
14
|
constructor() {
|
|
15
15
|
this.fileIndex = new Map();
|
|
@@ -523,10 +523,6 @@ class RegistryService {
|
|
|
523
523
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
524
524
|
}
|
|
525
525
|
getPersonalizedTopology() {
|
|
526
|
-
const personalizedRoot = (0, project_fraim_paths_1.getWorkspaceFraimPath)(process.cwd(), 'personalized-employee');
|
|
527
|
-
if (!(0, fs_1.existsSync)(personalizedRoot)) {
|
|
528
|
-
return { nodes: [], edges: [] };
|
|
529
|
-
}
|
|
530
526
|
const nodes = [];
|
|
531
527
|
const edges = [];
|
|
532
528
|
const seenNodeIds = new Set();
|
|
@@ -537,8 +533,12 @@ class RegistryService {
|
|
|
537
533
|
seenNodeIds.add(node.id);
|
|
538
534
|
nodes.push(node);
|
|
539
535
|
};
|
|
540
|
-
const
|
|
541
|
-
|
|
536
|
+
const skillRoots = (0, pack_home_1.personalizedCapabilityReadRoots)(process.cwd(), 'skills');
|
|
537
|
+
for (const root of skillRoots) {
|
|
538
|
+
const skillsRoot = root.capabilityRoot;
|
|
539
|
+
if (!(0, fs_1.existsSync)(skillsRoot)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
542
|
for (const relativePath of this.collectMarkdownRelativePaths(skillsRoot)) {
|
|
543
543
|
const normalizedRelativePath = normalize(relativePath);
|
|
544
544
|
const skillId = normalizedRelativePath.replace(/\.md$/, '');
|
|
@@ -551,13 +551,17 @@ class RegistryService {
|
|
|
551
551
|
category,
|
|
552
552
|
group: skillId.includes('/') ? skillId.slice(0, skillId.lastIndexOf('/')) : skillId,
|
|
553
553
|
role: null,
|
|
554
|
-
path: (
|
|
554
|
+
path: `${root.displayPrefix.replace(/\/$/, '')}/skills/${normalizedRelativePath}`,
|
|
555
555
|
shadowing: (0, fs_1.existsSync)(baselinePath)
|
|
556
556
|
});
|
|
557
557
|
}
|
|
558
558
|
}
|
|
559
|
-
const
|
|
560
|
-
|
|
559
|
+
const jobRoots = (0, pack_home_1.personalizedCapabilityReadRoots)(process.cwd(), 'jobs');
|
|
560
|
+
for (const root of jobRoots) {
|
|
561
|
+
const jobsRoot = root.capabilityRoot;
|
|
562
|
+
if (!(0, fs_1.existsSync)(jobsRoot)) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
561
565
|
for (const relativePath of this.collectMarkdownRelativePaths(jobsRoot)) {
|
|
562
566
|
const normalizedRelativePath = normalize(relativePath);
|
|
563
567
|
const jobPathWithoutExt = normalizedRelativePath.replace(/\.md$/, '');
|
|
@@ -574,7 +578,7 @@ class RegistryService {
|
|
|
574
578
|
category,
|
|
575
579
|
group: parts.slice(0, -1).join('/') || category,
|
|
576
580
|
role: category === 'ai-manager' ? 'ai-manager' : 'ai-employee',
|
|
577
|
-
path: (
|
|
581
|
+
path: `${root.displayPrefix.replace(/\/$/, '')}/jobs/${normalizedRelativePath}`,
|
|
578
582
|
shadowing: (0, fs_1.existsSync)(baselinePath)
|
|
579
583
|
});
|
|
580
584
|
const includes = [...content.matchAll(/\{\{include:skills\/([^}]+)\.md\}\}/g)];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.278",
|
|
4
4
|
"description": "FRAIM core CLI and MCP package.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"dist/src/services/",
|
|
20
20
|
"dist/src/api/",
|
|
21
21
|
"dist/src/middleware/",
|
|
22
|
-
"dist/src/models/",
|
|
23
|
-
"dist/src/
|
|
22
|
+
"dist/src/models/",
|
|
23
|
+
"dist/src/mcp/",
|
|
24
|
+
"dist/src/types/",
|
|
24
25
|
"dist/src/utils/",
|
|
25
26
|
"bin/fraim.js",
|
|
26
27
|
"public/first-run/",
|