fraim 2.0.277 → 2.0.279
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/commands/doctor.js +3 -1
- package/dist/src/cli/doctor/check-runner.js +12 -3
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +165 -0
- package/dist/src/cli/doctor/reporters/console-reporter.js +2 -1
- package/dist/src/cli/mcp/command-resolution.js +43 -43
- package/dist/src/cli/utils/managed-agent-install.js +55 -0
- package/dist/src/cli/utils/managed-agent-paths.js +55 -2
- package/dist/src/first-run/session-service.js +33 -58
- package/dist/src/mcp/tool-schemas.js +376 -0
- package/package.json +4 -3
|
@@ -15,6 +15,7 @@ const workflow_checks_1 = require("../doctor/checks/workflow-checks");
|
|
|
15
15
|
const ide_config_checks_1 = require("../doctor/checks/ide-config-checks");
|
|
16
16
|
const mcp_connectivity_checks_1 = require("../doctor/checks/mcp-connectivity-checks");
|
|
17
17
|
const scripts_checks_1 = require("../doctor/checks/scripts-checks");
|
|
18
|
+
const agent_cli_health_checks_1 = require("../doctor/checks/agent-cli-health-checks");
|
|
18
19
|
const add_ide_1 = require("./add-ide");
|
|
19
20
|
// Read version from package.json
|
|
20
21
|
const getFramVersion = () => {
|
|
@@ -58,7 +59,8 @@ function getAllChecks() {
|
|
|
58
59
|
...(0, workflow_checks_1.getJobChecks)(),
|
|
59
60
|
...(0, ide_config_checks_1.getIDEConfigChecks)(),
|
|
60
61
|
...(0, mcp_connectivity_checks_1.getMCPConnectivityChecks)(),
|
|
61
|
-
...(0, scripts_checks_1.getScriptsChecks)()
|
|
62
|
+
...(0, scripts_checks_1.getScriptsChecks)(),
|
|
63
|
+
...(0, agent_cli_health_checks_1.getAgentCliHealthChecks)()
|
|
62
64
|
];
|
|
63
65
|
}
|
|
64
66
|
async function runFixMcpRepair(options, repair = add_ide_1.runAddIDE) {
|
|
@@ -10,6 +10,10 @@ const CHECK_TIMEOUT = 2000; // 2 seconds per check
|
|
|
10
10
|
// Issue #532: stdio MCP servers need up to 15s for handshake + npm version resolution
|
|
11
11
|
// on first call via fraim-mcp-latest-launcher; 20s gives a 5s buffer.
|
|
12
12
|
const MCP_CHECK_TIMEOUT = 20000; // 20 seconds for MCP connectivity checks
|
|
13
|
+
// Issue #1285: this check shells out to `npm prefix -g`, up to three CLI
|
|
14
|
+
// `--version` probes, and (Windows) a PowerShell registry read — more real
|
|
15
|
+
// process spawns than the default budget comfortably covers.
|
|
16
|
+
const AGENT_CLI_HEALTH_CHECK_TIMEOUT = 10000; // 10 seconds for agent CLI health checks
|
|
13
17
|
const TOTAL_TIMEOUT = 30000; // 30 seconds total
|
|
14
18
|
// Simple logger for doctor command (optional, falls back to no-op)
|
|
15
19
|
const logger = {
|
|
@@ -39,8 +43,12 @@ const trackMetric = (name, value) => {
|
|
|
39
43
|
*/
|
|
40
44
|
async function runCheckWithTimeout(check, timeout = CHECK_TIMEOUT) {
|
|
41
45
|
const checkStartTime = Date.now();
|
|
42
|
-
// Use longer
|
|
43
|
-
const effectiveTimeout = check.category === 'mcpConnectivity'
|
|
46
|
+
// Use longer timeouts for checks that shell out to real CLI/registry probes.
|
|
47
|
+
const effectiveTimeout = check.category === 'mcpConnectivity'
|
|
48
|
+
? MCP_CHECK_TIMEOUT
|
|
49
|
+
: check.category === 'agentCliHealth'
|
|
50
|
+
? AGENT_CLI_HEALTH_CHECK_TIMEOUT
|
|
51
|
+
: timeout;
|
|
44
52
|
try {
|
|
45
53
|
logger.debug(`Running check: ${check.name}`, { category: check.category });
|
|
46
54
|
const result = await Promise.race([
|
|
@@ -132,7 +140,8 @@ async function runChecks(checks, options, version) {
|
|
|
132
140
|
jobs: { checks: [] },
|
|
133
141
|
ideConfiguration: { checks: [] },
|
|
134
142
|
mcpConnectivity: { checks: [] },
|
|
135
|
-
scripts: { checks: [] }
|
|
143
|
+
scripts: { checks: [] },
|
|
144
|
+
agentCliHealth: { checks: [] }
|
|
136
145
|
};
|
|
137
146
|
let passed = 0;
|
|
138
147
|
let warnings = 0;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent CLI PATH/version health checks for FRAIM doctor command
|
|
4
|
+
* Issue #1284/#1285: detect drift between FRAIM's own managed-PATH resolution,
|
|
5
|
+
* the ambient shell PATH, and the actual npm-global install for agent CLIs
|
|
6
|
+
* FRAIM installs through its managed-Node fallback.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getAgentCliHealthChecks = getAgentCliHealthChecks;
|
|
13
|
+
exports.checkAgentCliHealthByCommand = checkAgentCliHealthByCommand;
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const managed_agent_paths_1 = require("../../utils/managed-agent-paths");
|
|
17
|
+
const command_resolution_1 = require("../../mcp/command-resolution");
|
|
18
|
+
// Codex is FRAIM's first Hub-compatible CLI with a managed-install fallback
|
|
19
|
+
// (npm install -g into FRAIM's portable Node when no system install is
|
|
20
|
+
// found). Extend this list as claude/gemini/copilot gain the same fallback.
|
|
21
|
+
const MANAGED_CLIS = [
|
|
22
|
+
{ id: 'codex', label: 'Codex', command: 'codex' },
|
|
23
|
+
];
|
|
24
|
+
// Windows cannot CreateProcess a `.cmd`/`.bat` file directly (spawnSync on a
|
|
25
|
+
// resolved absolute `.cmd` path throws EINVAL) — it must go through cmd.exe,
|
|
26
|
+
// matching the same wrapping `hosts.ts`'s `resolveHostInvocation()` and
|
|
27
|
+
// `server.ts`'s `hubCommandVersion()` already use. Duplicated here (not
|
|
28
|
+
// imported from `hosts.ts`) because `src/cli` is a pure layer that cannot
|
|
29
|
+
// import the server-layer `src/ai-hub` module (`scripts/validate-purity.ts`).
|
|
30
|
+
function quoteWindowsArg(value) {
|
|
31
|
+
if (value.length === 0)
|
|
32
|
+
return '""';
|
|
33
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
|
|
34
|
+
}
|
|
35
|
+
function escapeWindowsArg(value) {
|
|
36
|
+
return /[\s"]/u.test(value) ? quoteWindowsArg(value) : value;
|
|
37
|
+
}
|
|
38
|
+
function probeVersion(commandPath) {
|
|
39
|
+
try {
|
|
40
|
+
const executable = process.platform === 'win32' ? 'cmd.exe' : commandPath;
|
|
41
|
+
const args = process.platform === 'win32'
|
|
42
|
+
? ['/d', '/s', '/c', `${escapeWindowsArg(commandPath)} --version`]
|
|
43
|
+
: ['--version'];
|
|
44
|
+
const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000 });
|
|
45
|
+
if (result.status !== 0 || result.error)
|
|
46
|
+
return null;
|
|
47
|
+
return (result.stdout || result.stderr || '').trim() || null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// The persisted Windows User PATH (what a brand-new terminal reads) can
|
|
54
|
+
// disagree with this process's own inherited `process.env.PATH` (what a
|
|
55
|
+
// shell already open before the fix landed still uses) — issue #1285's
|
|
56
|
+
// exact "update said success but --version looked wrong" ambiguity.
|
|
57
|
+
// Windows-only; there is no equivalent persisted-PATH registry on macOS/Linux.
|
|
58
|
+
function readPersistedUserPath() {
|
|
59
|
+
if (process.platform !== 'win32')
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', "[Environment]::GetEnvironmentVariable('PATH','User')"], { encoding: 'utf8', timeout: 5000 });
|
|
63
|
+
if (result.status !== 0 || result.error)
|
|
64
|
+
return null;
|
|
65
|
+
return (result.stdout || '').trim() || null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function runAgentCliHealthCheck(cli) {
|
|
72
|
+
// "Ambient" is this process's own raw, unmodified PATH — exactly what a
|
|
73
|
+
// shell already open before a PATH fix landed still sees (issue #1285's
|
|
74
|
+
// "any shell the user already has open keeps its own stale in-memory copy
|
|
75
|
+
// until restarted" applies to the process running this check too, not
|
|
76
|
+
// only the user's terminal). "Managed" strips any stale managed directory
|
|
77
|
+
// already sitting on that ambient PATH and re-appends FRAIM's current,
|
|
78
|
+
// correctly-ordered managed dirs — so it always reflects the current
|
|
79
|
+
// versioned build, independent of whatever the ambient PATH happens to
|
|
80
|
+
// still contain. Deliberately NOT `resolveManagedCommand()`: that helper
|
|
81
|
+
// is system-PATH-first for launch purposes and would silently agree with
|
|
82
|
+
// a stale ambient entry instead of surfacing the drift this check exists
|
|
83
|
+
// to catch.
|
|
84
|
+
const ambientPath = (0, command_resolution_1.getSystemCommandPath)(cli.command);
|
|
85
|
+
const managedSearchPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
86
|
+
const managedPath = (0, command_resolution_1.getSystemCommandPath)(cli.command, managedSearchPath);
|
|
87
|
+
if (!ambientPath && !managedPath) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'passed',
|
|
90
|
+
message: `${cli.label} is not installed; nothing to check.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const ambientVersion = ambientPath ? probeVersion(ambientPath) : null;
|
|
94
|
+
const managedVersion = !managedPath
|
|
95
|
+
? null
|
|
96
|
+
: managedPath === ambientPath
|
|
97
|
+
? ambientVersion
|
|
98
|
+
: probeVersion(managedPath);
|
|
99
|
+
const npmGlobalBinDirs = (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)();
|
|
100
|
+
const npmGlobalPath = npmGlobalBinDirs.length > 0
|
|
101
|
+
? (0, command_resolution_1.getSystemCommandPath)(cli.command, npmGlobalBinDirs.join(path_1.default.delimiter))
|
|
102
|
+
: null;
|
|
103
|
+
const npmGlobalVersion = npmGlobalPath ? probeVersion(npmGlobalPath) : null;
|
|
104
|
+
// Drift means either: the ambient shell resolves a different version than
|
|
105
|
+
// the actual npm-global install, or this process's own ambient PATH
|
|
106
|
+
// resolves a *different file* than FRAIM's freshly-recomputed managed-dir
|
|
107
|
+
// resolution would — the exact "stale shim still shadows the current one"
|
|
108
|
+
// signature from #1285.
|
|
109
|
+
const versionMismatch = Boolean((ambientVersion && npmGlobalVersion && ambientVersion !== npmGlobalVersion)
|
|
110
|
+
|| (ambientPath && managedPath && ambientPath !== managedPath));
|
|
111
|
+
if (!versionMismatch) {
|
|
112
|
+
return {
|
|
113
|
+
status: 'passed',
|
|
114
|
+
message: `${cli.label} is consistent (${ambientVersion || managedVersion || 'unknown version'}).`,
|
|
115
|
+
details: { ambientPath, managedPath, npmGlobalPath },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const persistedUserPath = readPersistedUserPath();
|
|
119
|
+
const persistedResolved = persistedUserPath ? (0, command_resolution_1.getSystemCommandPath)(cli.command, persistedUserPath) : null;
|
|
120
|
+
const persistedVersion = persistedResolved ? probeVersion(persistedResolved) : null;
|
|
121
|
+
const persistedMatchesManaged = Boolean(persistedResolved && managedPath && persistedResolved === managedPath);
|
|
122
|
+
const suggestion = persistedUserPath === null
|
|
123
|
+
? `Restart your terminal, then run "${cli.command} --version" again to confirm.`
|
|
124
|
+
: persistedMatchesManaged
|
|
125
|
+
? `Your saved PATH is already correct — open a new terminal window so it takes effect, then run "${cli.command} --version" again.`
|
|
126
|
+
: `Your saved PATH still resolves the old ${cli.label} build. Run "${cli.command} update" (or reinstall ${cli.label}), then open a new terminal.`;
|
|
127
|
+
return {
|
|
128
|
+
status: 'warning',
|
|
129
|
+
message: `${cli.label} PATH drift detected: ambient ${ambientVersion || 'not found'} vs managed ${managedVersion || 'not found'} vs npm-global ${npmGlobalVersion || 'not found'}.`,
|
|
130
|
+
suggestion,
|
|
131
|
+
details: {
|
|
132
|
+
ambientPath, ambientVersion,
|
|
133
|
+
managedPath, managedVersion,
|
|
134
|
+
npmGlobalPath, npmGlobalVersion,
|
|
135
|
+
persistedUserPathResolvedTo: persistedResolved,
|
|
136
|
+
persistedUserPathVersion: persistedVersion,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function agentCliHealthCheck(cli) {
|
|
141
|
+
return {
|
|
142
|
+
name: `${cli.label} PATH/version health`,
|
|
143
|
+
category: 'agentCliHealth',
|
|
144
|
+
critical: false,
|
|
145
|
+
run: () => runAgentCliHealthCheck(cli),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function getAgentCliHealthChecks() {
|
|
149
|
+
return MANAGED_CLIS.map(agentCliHealthCheck);
|
|
150
|
+
}
|
|
151
|
+
// Reused by the Hub's `/api/ai-hub/check-agent` route (issue #1285 §4) so a
|
|
152
|
+
// manager who clicks "Check if Ready" right after `codex update` sees PATH
|
|
153
|
+
// drift immediately instead of a bare pass/fail. Returns null for a CLI this
|
|
154
|
+
// module does not track.
|
|
155
|
+
async function checkAgentCliHealthByCommand(command) {
|
|
156
|
+
const cli = MANAGED_CLIS.find((entry) => entry.command === command);
|
|
157
|
+
if (!cli)
|
|
158
|
+
return null;
|
|
159
|
+
try {
|
|
160
|
+
return await runAgentCliHealthCheck(cli);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -16,7 +16,8 @@ const CATEGORY_NAMES = {
|
|
|
16
16
|
jobs: 'Jobs',
|
|
17
17
|
ideConfiguration: 'IDE Configuration',
|
|
18
18
|
mcpConnectivity: 'MCP Server Connectivity',
|
|
19
|
-
scripts: 'Scripts'
|
|
19
|
+
scripts: 'Scripts',
|
|
20
|
+
agentCliHealth: 'Agent CLI Health'
|
|
20
21
|
};
|
|
21
22
|
function getStatusIcon(status) {
|
|
22
23
|
switch (status) {
|
|
@@ -3,47 +3,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.resolveManagedCommand = exports.
|
|
6
|
+
exports.resolveManagedCommand = exports.getPortableManagedCommandPath = exports.getSystemCommandPath = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.sort((a, b) => b.name.localeCompare(a.name));
|
|
19
|
-
for (const entry of extractedDirs) {
|
|
20
|
-
candidates.push(path_1.default.join(nodeRoot, entry.name, 'npx.cmd'));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return candidates;
|
|
24
|
-
}
|
|
25
|
-
return [
|
|
26
|
-
path_1.default.join(nodeRoot, 'bin', 'npx'),
|
|
27
|
-
path_1.default.join(nodeRoot, 'npx')
|
|
28
|
-
];
|
|
29
|
-
};
|
|
30
|
-
const getPortableNpxCommand = () => {
|
|
31
|
-
for (const candidate of getPortableNpxCandidates()) {
|
|
32
|
-
if (fs_1.default.existsSync(candidate)) {
|
|
33
|
-
return candidate;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
};
|
|
38
|
-
exports.getPortableNpxCommand = getPortableNpxCommand;
|
|
39
|
-
const getPathEntries = () => {
|
|
40
|
-
const rawPath = process.env.PATH || '';
|
|
9
|
+
const managed_agent_paths_1 = require("../utils/managed-agent-paths");
|
|
10
|
+
// Issue #1284/#1285 (Implementation Strategy §0): commands FRAIM resolves
|
|
11
|
+
// fresh at launch time instead of trusting a persisted PATH string. Started
|
|
12
|
+
// as `npx`-only; generalized to every agent CLI FRAIM itself launches
|
|
13
|
+
// (Hub-driven runs, version probes, install/check-agent routes) so those
|
|
14
|
+
// launches are immune to PATH-order drift structurally.
|
|
15
|
+
const MANAGED_COMMANDS = new Set(['npx', 'codex', 'claude', 'gemini', 'copilot']);
|
|
16
|
+
const getPathEntries = (basePath) => {
|
|
17
|
+
const rawPath = (0, managed_agent_paths_1.stripProjectLocalNodeBinDirs)(basePath ?? process.env.PATH ?? '');
|
|
41
18
|
return rawPath
|
|
42
19
|
.split(path_1.default.delimiter)
|
|
43
20
|
.map((entry) => entry.trim())
|
|
44
21
|
.filter(Boolean);
|
|
45
22
|
};
|
|
46
|
-
const getSystemCommandCandidates = (command) => {
|
|
23
|
+
const getSystemCommandCandidates = (command, basePath) => {
|
|
47
24
|
if (!command || path_1.default.isAbsolute(command)) {
|
|
48
25
|
return command ? [command] : [];
|
|
49
26
|
}
|
|
@@ -52,10 +29,14 @@ const getSystemCommandCandidates = (command) => {
|
|
|
52
29
|
? [command]
|
|
53
30
|
: [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`, `${command}.com`]
|
|
54
31
|
: [command];
|
|
55
|
-
return getPathEntries().flatMap((entry) => commandNames.map((name) => path_1.default.join(entry, name)));
|
|
32
|
+
return getPathEntries(basePath).flatMap((entry) => commandNames.map((name) => path_1.default.join(entry, name)));
|
|
56
33
|
};
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
// `basePath` defaults to the current process PATH; pass an explicit PATH
|
|
35
|
+
// string to resolve a command against a different candidate list (e.g. the
|
|
36
|
+
// npm-global bin dirs computed by `resolveNpmGlobalBinDirs`) without mutating
|
|
37
|
+
// `process.env.PATH`.
|
|
38
|
+
const getSystemCommandPath = (command, basePath) => {
|
|
39
|
+
for (const candidate of getSystemCommandCandidates(command, basePath)) {
|
|
59
40
|
try {
|
|
60
41
|
const stats = fs_1.default.statSync(candidate);
|
|
61
42
|
if (stats.isFile()) {
|
|
@@ -69,13 +50,32 @@ const getSystemCommandPath = (command) => {
|
|
|
69
50
|
return null;
|
|
70
51
|
};
|
|
71
52
|
exports.getSystemCommandPath = getSystemCommandPath;
|
|
53
|
+
// Mirrors the shape of the old `getPortableNpxCandidates()`, but checks only
|
|
54
|
+
// the current versioned `getPortableNodeBinPath()` directory — never the
|
|
55
|
+
// legacy flat directory — so a shim orphaned there (Defect A) can never be
|
|
56
|
+
// selected here regardless of what's on disk.
|
|
57
|
+
const getPortableManagedCommandCandidates = (command) => {
|
|
58
|
+
const versionedDir = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
return [`${command}.cmd`, `${command}.exe`].map((name) => path_1.default.join(versionedDir, name));
|
|
61
|
+
}
|
|
62
|
+
return [path_1.default.join(versionedDir, command)];
|
|
63
|
+
};
|
|
64
|
+
const getPortableManagedCommandPath = (command) => {
|
|
65
|
+
for (const candidate of getPortableManagedCommandCandidates(command)) {
|
|
66
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
exports.getPortableManagedCommandPath = getPortableManagedCommandPath;
|
|
72
73
|
const resolveManagedCommand = (command) => {
|
|
73
|
-
if (command
|
|
74
|
+
if (!MANAGED_COMMANDS.has(command))
|
|
74
75
|
return command;
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
return (0, exports.getSystemCommandPath)(command) || (0, exports.getPortableNpxCommand)() || command;
|
|
76
|
+
// Prefer a system-installed CLI so FRAIM doesn't install its own copy when
|
|
77
|
+
// the machine already has one. Fall back to the FRAIM-managed portable copy
|
|
78
|
+
// only when no system install is found. Last resort: bare command name.
|
|
79
|
+
return (0, exports.getSystemCommandPath)(command) || (0, exports.getPortableManagedCommandPath)(command) || command;
|
|
80
80
|
};
|
|
81
81
|
exports.resolveManagedCommand = resolveManagedCommand;
|
|
@@ -0,0 +1,55 @@
|
|
|
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.installManagedAgent = installManagedAgent;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
9
|
+
// Issue #1284/#1285 (Implementation Strategy §2): the exact "standard npm
|
|
10
|
+
// global install, then fall back to FRAIM's managed prefix" sequence used to
|
|
11
|
+
// be duplicated verbatim in `server.ts` (Hub's install-agent route) and
|
|
12
|
+
// `session-service.ts` (first-run's installAgent), including Defect A — both
|
|
13
|
+
// pointed `npm_config_prefix` at the flat `nodeRoot` dir instead of the
|
|
14
|
+
// versioned dir that actually contains the Node/npm binaries running the
|
|
15
|
+
// install. Extracted so the prefix fix (and any future fix to this sequence)
|
|
16
|
+
// lives in one place. Callers are responsible for the "already installed"
|
|
17
|
+
// check before invoking this — it only covers the install-then-fallback path.
|
|
18
|
+
async function installManagedAgent(option, systemPath, deps) {
|
|
19
|
+
let standardInstallError = null;
|
|
20
|
+
try {
|
|
21
|
+
await deps.runProcess('npm', ['install', '-g', option.installPackage], {
|
|
22
|
+
PATH: systemPath,
|
|
23
|
+
npm_config_prefix: undefined,
|
|
24
|
+
NPM_CONFIG_PREFIX: undefined,
|
|
25
|
+
});
|
|
26
|
+
const standardVersion = deps.commandVersion(option.launchCommand, undefined, systemPath);
|
|
27
|
+
const npmGlobalBinDirs = standardVersion
|
|
28
|
+
? []
|
|
29
|
+
: (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)(systemPath, {
|
|
30
|
+
npm_config_prefix: undefined,
|
|
31
|
+
NPM_CONFIG_PREFIX: undefined,
|
|
32
|
+
});
|
|
33
|
+
const standardVersionWithNpmBin = standardVersion
|
|
34
|
+
|| (npmGlobalBinDirs.length > 0
|
|
35
|
+
? deps.commandVersion(option.launchCommand, npmGlobalBinDirs, systemPath)
|
|
36
|
+
: null);
|
|
37
|
+
if (standardVersionWithNpmBin) {
|
|
38
|
+
return { outcome: 'standard', npmGlobalBinDirs };
|
|
39
|
+
}
|
|
40
|
+
standardInstallError = `${option.label} standard install completed, but the CLI is not runnable from the user PATH.`;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
standardInstallError = error instanceof Error ? error.message : 'Unknown error';
|
|
44
|
+
}
|
|
45
|
+
// Defect A fix: co-locate npm-global shims with the node.exe/npm.cmd that
|
|
46
|
+
// actually runs the install, instead of the flat legacy directory.
|
|
47
|
+
const prefix = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
48
|
+
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
49
|
+
await deps.runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
50
|
+
const ver = deps.commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
51
|
+
if (!ver) {
|
|
52
|
+
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH. Standard install failure: ${standardInstallError}`);
|
|
53
|
+
}
|
|
54
|
+
return { outcome: 'managed' };
|
|
55
|
+
}
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.getManagedNodeRoot = getManagedNodeRoot;
|
|
7
7
|
exports.getPortableNodeBinPath = getPortableNodeBinPath;
|
|
8
8
|
exports.getManagedAgentBinDirs = getManagedAgentBinDirs;
|
|
9
|
+
exports.stripProjectLocalNodeBinDirs = stripProjectLocalNodeBinDirs;
|
|
10
|
+
exports.cleanupOrphanedManagedShims = cleanupOrphanedManagedShims;
|
|
9
11
|
exports.stripManagedAgentBinDirsFromPath = stripManagedAgentBinDirsFromPath;
|
|
10
12
|
exports.appendBinDirsToPath = appendBinDirsToPath;
|
|
11
13
|
exports.getNpmGlobalBinDirsFromPrefix = getNpmGlobalBinDirsFromPrefix;
|
|
@@ -37,11 +39,62 @@ function getPortableNodeBinPath() {
|
|
|
37
39
|
function getManagedAgentBinDirs() {
|
|
38
40
|
const nodeRoot = getManagedNodeRoot();
|
|
39
41
|
const portableNodeBin = getPortableNodeBinPath();
|
|
42
|
+
// The current versioned Node directory must outrank the legacy flat directory:
|
|
43
|
+
// a shim orphaned in the flat directory by an older FRAIM version must never
|
|
44
|
+
// shadow a newer bundled build (issue #1285). uniquePathEntries'/Set-based dedup
|
|
45
|
+
// collapses this to a single entry when no versioned subfolder exists yet.
|
|
40
46
|
const candidates = process.platform === 'win32'
|
|
41
|
-
? [
|
|
42
|
-
: [nodeRoot, path_1.default.join(nodeRoot, 'bin')
|
|
47
|
+
? [portableNodeBin, nodeRoot]
|
|
48
|
+
: [portableNodeBin, nodeRoot, path_1.default.join(nodeRoot, 'bin')];
|
|
43
49
|
return [...new Set(candidates.filter(Boolean))];
|
|
44
50
|
}
|
|
51
|
+
// Issue #1284/#1285: strips PATH entries that point at a project-local
|
|
52
|
+
// `node_modules/.bin` directory. Relocated from `src/ai-hub/hosts.ts` (Defect D)
|
|
53
|
+
// so the pure `src/cli` layer (which `command-resolution.ts` belongs to, and
|
|
54
|
+
// which cannot import from the server-layer `hosts.ts`) can apply the same
|
|
55
|
+
// filtering when resolving a managed command, not just when probing agent
|
|
56
|
+
// versions. A devDependency's own CLI shim (e.g. a stale `@openai/codex-sdk`
|
|
57
|
+
// pulled in transitively) must never shadow the real global install.
|
|
58
|
+
function stripProjectLocalNodeBinDirs(basePath) {
|
|
59
|
+
return (basePath ?? '')
|
|
60
|
+
.split(path_1.default.delimiter)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.filter((entry) => {
|
|
63
|
+
const normalized = path_1.default.normalize(entry).toLowerCase();
|
|
64
|
+
return !normalized.endsWith(`${path_1.default.sep}node_modules${path_1.default.sep}.bin`);
|
|
65
|
+
})
|
|
66
|
+
.join(path_1.default.delimiter);
|
|
67
|
+
}
|
|
68
|
+
// Issue #1285 (Implementation Strategy §3): after (re)ordering the managed
|
|
69
|
+
// candidates above, nothing new is ever written to the flat, legacy `nodeRoot`
|
|
70
|
+
// directory — but installs made before this fix left real shims sitting there.
|
|
71
|
+
// Scoped strictly to filenames npm itself would have generated for a managed
|
|
72
|
+
// agent CLI install (never a wildcard sweep), so this can run unconditionally
|
|
73
|
+
// and idempotently on every FRAIM startup.
|
|
74
|
+
const MANAGED_AGENT_COMMANDS = ['codex', 'claude', 'gemini', 'copilot'];
|
|
75
|
+
function cleanupOrphanedManagedShims() {
|
|
76
|
+
const nodeRoot = getManagedNodeRoot();
|
|
77
|
+
const versionedDir = getPortableNodeBinPath();
|
|
78
|
+
if (versionedDir === nodeRoot || !fs_1.default.existsSync(nodeRoot))
|
|
79
|
+
return [];
|
|
80
|
+
const basenames = process.platform === 'win32'
|
|
81
|
+
? MANAGED_AGENT_COMMANDS.flatMap((command) => [`${command}.cmd`, `${command}.ps1`])
|
|
82
|
+
: MANAGED_AGENT_COMMANDS;
|
|
83
|
+
const removed = [];
|
|
84
|
+
for (const basename of basenames) {
|
|
85
|
+
const candidate = path_1.default.join(nodeRoot, basename);
|
|
86
|
+
try {
|
|
87
|
+
if (fs_1.default.statSync(candidate).isFile()) {
|
|
88
|
+
fs_1.default.unlinkSync(candidate);
|
|
89
|
+
removed.push(candidate);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Not present or inaccessible — nothing to clean up.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
45
98
|
function normalizePathEntry(entry) {
|
|
46
99
|
const resolved = path_1.default.resolve(entry.trim());
|
|
47
100
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.FIRST_RUN_ROW_IDS = exports.FirstRunSessionService = void 0;
|
|
40
|
+
exports.buildPersistShellPathWindowsCommand = buildPersistShellPathWindowsCommand;
|
|
40
41
|
exports.buildFirstRunHubLaunchArgs = buildFirstRunHubLaunchArgs;
|
|
41
42
|
const fs_1 = __importDefault(require("fs"));
|
|
42
43
|
const os_1 = __importDefault(require("os"));
|
|
@@ -49,6 +50,7 @@ const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
|
49
50
|
const setup_1 = require("../cli/commands/setup");
|
|
50
51
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
51
52
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
53
|
+
const managed_agent_install_1 = require("../cli/utils/managed-agent-install");
|
|
52
54
|
const types_1 = require("./types");
|
|
53
55
|
Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
|
|
54
56
|
const install_state_1 = require("./install-state");
|
|
@@ -89,26 +91,40 @@ function ensureOutputDirs() {
|
|
|
89
91
|
// installed there without shadowing user/system installs.
|
|
90
92
|
(function bootstrapFraimNodeBin() {
|
|
91
93
|
(0, managed_agent_paths_1.appendManagedAgentBinDirsToProcessPath)();
|
|
94
|
+
// Issue #1285 (Implementation Strategy §3): remove any managed-agent shim
|
|
95
|
+
// orphaned in the legacy flat directory by an older FRAIM version, so it
|
|
96
|
+
// can no longer shadow the current versioned build.
|
|
97
|
+
const removedShims = (0, managed_agent_paths_1.cleanupOrphanedManagedShims)();
|
|
98
|
+
if (removedShims.length > 0) {
|
|
99
|
+
appendInstallLog(`orphaned-managed-shims-removed ${removedShims.join(', ')}`);
|
|
100
|
+
}
|
|
92
101
|
})();
|
|
102
|
+
// Issue #1285: pure command-string builder extracted from persistShellPath()
|
|
103
|
+
// so the generated PowerShell command's directory order (the current
|
|
104
|
+
// versioned dir added before the legacy flat dir, per `bins`' own order —
|
|
105
|
+
// see getManagedAgentBinDirs()) can be asserted directly, without mutating
|
|
106
|
+
// the real registry-backed Windows User PATH in a test.
|
|
107
|
+
function buildPersistShellPathWindowsCommand(bins) {
|
|
108
|
+
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
109
|
+
const removals = bins.map((_, index) => `$parts = @($parts | Where-Object { $_ -ne $bin${index} })`);
|
|
110
|
+
const additions = bins.map((_, index) => `$parts += $bin${index}`);
|
|
111
|
+
return [
|
|
112
|
+
...assignments,
|
|
113
|
+
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
114
|
+
`$parts = @($cur -split ';' | Where-Object { $_ })`,
|
|
115
|
+
...removals,
|
|
116
|
+
...additions,
|
|
117
|
+
`$cur = ($parts | Select-Object -Unique) -join ';'`,
|
|
118
|
+
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
119
|
+
].join('; ');
|
|
120
|
+
}
|
|
93
121
|
function persistShellPath() {
|
|
94
122
|
const marker = '# FRAIM managed binaries';
|
|
95
123
|
const legacyExportLine = 'export PATH="$HOME/.fraim/node/bin:$PATH"';
|
|
96
124
|
const exportLine = 'export PATH="$PATH:$HOME/.fraim/node/bin"';
|
|
97
125
|
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
98
126
|
if (process.platform === 'win32') {
|
|
99
|
-
const
|
|
100
|
-
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
101
|
-
const removals = bins.map((_, index) => `$parts = @($parts | Where-Object { $_ -ne $bin${index} })`);
|
|
102
|
-
const additions = bins.map((_, index) => `$parts += $bin${index}`);
|
|
103
|
-
const psCmd = [
|
|
104
|
-
...assignments,
|
|
105
|
-
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
106
|
-
`$parts = @($cur -split ';' | Where-Object { $_ })`,
|
|
107
|
-
...removals,
|
|
108
|
-
...additions,
|
|
109
|
-
`$cur = ($parts | Select-Object -Unique) -join ';'`,
|
|
110
|
-
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
111
|
-
].join('; ');
|
|
127
|
+
const psCmd = buildPersistShellPathWindowsCommand((0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
112
128
|
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
113
129
|
return;
|
|
114
130
|
}
|
|
@@ -782,53 +798,12 @@ class FirstRunSessionService {
|
|
|
782
798
|
loginHint: `Sign in to ${option.label} to activate it. A terminal window will open with the sign-in command — complete sign-in there, then return here and click "Check if Ready".`,
|
|
783
799
|
};
|
|
784
800
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
PATH: systemPath,
|
|
789
|
-
npm_config_prefix: undefined,
|
|
790
|
-
NPM_CONFIG_PREFIX: undefined,
|
|
791
|
-
});
|
|
792
|
-
const standardVersion = commandVersion(option.launchCommand, undefined, systemPath);
|
|
793
|
-
const npmGlobalBinDirs = standardVersion
|
|
794
|
-
? []
|
|
795
|
-
: (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)(systemPath, {
|
|
796
|
-
npm_config_prefix: undefined,
|
|
797
|
-
NPM_CONFIG_PREFIX: undefined,
|
|
798
|
-
});
|
|
799
|
-
const standardVersionWithNpmBin = standardVersion
|
|
800
|
-
|| (npmGlobalBinDirs.length > 0
|
|
801
|
-
? commandVersion(option.launchCommand, npmGlobalBinDirs, systemPath)
|
|
802
|
-
: null);
|
|
803
|
-
if (standardVersionWithNpmBin) {
|
|
804
|
-
if (npmGlobalBinDirs.length > 0) {
|
|
805
|
-
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, npmGlobalBinDirs);
|
|
806
|
-
}
|
|
807
|
-
this.setAgentInstallStatus(agentId, 'needs-sign-in', `Sign in to ${option.label} to activate it.`);
|
|
808
|
-
appendInstallLog(`agent-installed-standard ${agentId}`);
|
|
809
|
-
this.persist();
|
|
810
|
-
return {
|
|
811
|
-
ok: true,
|
|
812
|
-
message: `${option.label} installed successfully.`,
|
|
813
|
-
needsLogin: true,
|
|
814
|
-
loginCommand: option.loginCommand,
|
|
815
|
-
loginHint: `Sign in to ${option.label} to activate it. A terminal window will open with the sign-in command — complete sign-in there, then return here and click "Check if Ready".`,
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
standardInstallError = `${option.label} standard install completed, but the CLI is not runnable from the user PATH.`;
|
|
819
|
-
}
|
|
820
|
-
catch (error) {
|
|
821
|
-
standardInstallError = error instanceof Error ? error.message : 'Unknown error';
|
|
822
|
-
}
|
|
823
|
-
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
824
|
-
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
825
|
-
await runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
826
|
-
const ver = commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
827
|
-
if (!ver) {
|
|
828
|
-
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH. Standard install failure: ${standardInstallError}`);
|
|
801
|
+
const outcome = await (0, managed_agent_install_1.installManagedAgent)({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath, { runProcess, commandVersion });
|
|
802
|
+
if (outcome.outcome === 'standard' && outcome.npmGlobalBinDirs.length > 0) {
|
|
803
|
+
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, outcome.npmGlobalBinDirs);
|
|
829
804
|
}
|
|
830
805
|
this.setAgentInstallStatus(agentId, 'needs-sign-in', `Sign in to ${option.label} to activate it.`);
|
|
831
|
-
appendInstallLog(`agent-installed-managed ${agentId}`);
|
|
806
|
+
appendInstallLog(outcome.outcome === 'standard' ? `agent-installed-standard ${agentId}` : `agent-installed-managed ${agentId}`);
|
|
832
807
|
this.persist();
|
|
833
808
|
return {
|
|
834
809
|
ok: true,
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.279",
|
|
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/",
|