fraim-framework 2.0.124 → 2.0.127
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/bin/fraim.js +1 -1
- package/dist/src/ai-hub/catalog.js +280 -44
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/hosts.js +384 -10
- package/dist/src/ai-hub/server.js +255 -9
- package/dist/src/cli/commands/add-ide.js +4 -3
- package/dist/src/cli/commands/first-run.js +61 -0
- package/dist/src/cli/commands/hub.js +4 -4
- package/dist/src/cli/commands/init-project.js +4 -4
- package/dist/src/cli/commands/setup.js +4 -3
- package/dist/src/cli/commands/sync.js +21 -2
- package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +29 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
- package/dist/src/cli/setup/ide-detector.js +32 -1
- package/dist/src/cli/setup/ide-global-integration.js +5 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +70 -17
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +12 -2
- package/dist/src/cli/utils/project-bootstrap.js +4 -3
- package/dist/src/core/quality-evidence.js +81 -8
- package/dist/src/core/utils/git-utils.js +32 -7
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +3 -5
- package/dist/src/first-run/install-state.js +68 -0
- package/dist/src/first-run/server.js +153 -0
- package/dist/src/first-run/session-service.js +302 -0
- package/dist/src/first-run/types.js +40 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +114 -0
- package/dist/src/local-mcp-server/codex-token-adapter.js +232 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +21 -8
- package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
- package/dist/src/local-mcp-server/stdio-server.js +70 -17
- package/dist/src/local-mcp-server/token-adapter-registry.js +64 -0
- package/dist/src/local-mcp-server/usage-collector.js +25 -0
- package/index.js +83 -83
- package/package.json +7 -1
- package/public/ai-hub/index.html +149 -102
- package/public/ai-hub/script.js +1154 -271
- package/public/ai-hub/styles.css +753 -450
- package/public/first-run/index.html +221 -0
- package/public/first-run/script.js +361 -0
- package/dist/src/cli/services/device-flow-service.js +0 -83
- package/dist/src/local-mcp-server/prometheus-scraper.js +0 -152
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = void 0;
|
|
4
|
+
exports.resolveFraimJobName = resolveFraimJobName;
|
|
5
|
+
exports.isListableFraimJob = isListableFraimJob;
|
|
6
|
+
const DEPRECATED_TO_CANONICAL_JOB_MAP = {
|
|
7
|
+
'learn-and-scale': 'upskill-employee',
|
|
8
|
+
'model-behavior': 'upskill-employee',
|
|
9
|
+
'promote-learning': 'upskill-employee',
|
|
10
|
+
'refine-jobs': 'upskill-employee',
|
|
11
|
+
'refine-skills': 'upskill-employee'
|
|
12
|
+
};
|
|
13
|
+
const DIRECT_JOB_ALIASES = {
|
|
14
|
+
'sleep on learnings': 'sleep-on-learnings'
|
|
15
|
+
};
|
|
16
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = new Set(Object.keys(DEPRECATED_TO_CANONICAL_JOB_MAP));
|
|
17
|
+
function normalizeJobLookupInput(input) {
|
|
18
|
+
return input.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
19
|
+
}
|
|
20
|
+
function resolveFraimJobName(input) {
|
|
21
|
+
const normalizedJobName = normalizeJobLookupInput(input);
|
|
22
|
+
const directAliasTarget = DIRECT_JOB_ALIASES[input.trim().toLowerCase()];
|
|
23
|
+
if (directAliasTarget) {
|
|
24
|
+
return {
|
|
25
|
+
requestedJobName: input,
|
|
26
|
+
normalizedJobName,
|
|
27
|
+
canonicalJobName: directAliasTarget
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const deprecatedAliasTarget = DEPRECATED_TO_CANONICAL_JOB_MAP[normalizedJobName];
|
|
31
|
+
if (deprecatedAliasTarget) {
|
|
32
|
+
return {
|
|
33
|
+
requestedJobName: input,
|
|
34
|
+
normalizedJobName,
|
|
35
|
+
canonicalJobName: deprecatedAliasTarget,
|
|
36
|
+
deprecatedAliasTarget
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
requestedJobName: input,
|
|
41
|
+
normalizedJobName,
|
|
42
|
+
canonicalJobName: normalizedJobName
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isListableFraimJob(jobName) {
|
|
46
|
+
return !exports.DEPRECATED_FRAIM_JOB_NAMES.has(normalizeJobLookupInput(jobName));
|
|
47
|
+
}
|
|
@@ -5,15 +5,13 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
7
|
static extractMetadataBlock(content) {
|
|
8
|
-
|
|
9
|
-
const frontmatterMatch = content.match(/^[\s\S]*?---\r?\n([\s\S]+?)\r?\n---/);
|
|
8
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
10
9
|
if (frontmatterMatch) {
|
|
11
10
|
try {
|
|
12
|
-
const startIndex = frontmatterMatch.index || 0;
|
|
13
11
|
return {
|
|
14
12
|
state: 'valid',
|
|
15
13
|
metadata: JSON.parse(frontmatterMatch[1]),
|
|
16
|
-
bodyStartIndex:
|
|
14
|
+
bodyStartIndex: frontmatterMatch[0].length
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
17
|
catch {
|
|
@@ -109,7 +107,7 @@ class WorkflowParser {
|
|
|
109
107
|
overview = contentAfterMetadata;
|
|
110
108
|
}
|
|
111
109
|
const phases = new Map();
|
|
112
|
-
const phaseSections = restOfContent.split(/^##\s+Phase:\s
|
|
110
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
113
111
|
if (!metadata.phases) {
|
|
114
112
|
metadata.phases = {};
|
|
115
113
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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.createInitialFirstRunState = createInitialFirstRunState;
|
|
7
|
+
exports.loadFirstRunState = loadFirstRunState;
|
|
8
|
+
exports.saveFirstRunState = saveFirstRunState;
|
|
9
|
+
exports.clearFirstRunState = clearFirstRunState;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
13
|
+
const types_1 = require("./types");
|
|
14
|
+
function getStatePath() {
|
|
15
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-state.json');
|
|
16
|
+
}
|
|
17
|
+
function ensureUserDir() {
|
|
18
|
+
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
function maskInstallKey(key) {
|
|
21
|
+
const normalized = key.trim();
|
|
22
|
+
if (normalized.length <= 8) {
|
|
23
|
+
return '<redacted>';
|
|
24
|
+
}
|
|
25
|
+
return `${normalized.slice(0, 4)}...${normalized.slice(-4)}`;
|
|
26
|
+
}
|
|
27
|
+
function createInitialFirstRunState(key) {
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
return {
|
|
30
|
+
version: 1,
|
|
31
|
+
installKeyRef: maskInstallKey(key),
|
|
32
|
+
platform: process.platform,
|
|
33
|
+
detectedAgents: [],
|
|
34
|
+
configuredAgents: [],
|
|
35
|
+
restartDeferredAgents: [],
|
|
36
|
+
resourcesUrl: types_1.FIRST_RUN_RESOURCES_URL,
|
|
37
|
+
stepStates: (0, types_1.createDefaultStepStates)(),
|
|
38
|
+
createdAt: now,
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function loadFirstRunState() {
|
|
43
|
+
const statePath = getStatePath();
|
|
44
|
+
if (!fs_1.default.existsSync(statePath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const state = JSON.parse(fs_1.default.readFileSync(statePath, 'utf8'));
|
|
49
|
+
if (typeof state.installKeyRef === 'string' && !state.installKeyRef.includes('...')) {
|
|
50
|
+
state.installKeyRef = maskInstallKey(state.installKeyRef);
|
|
51
|
+
}
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function saveFirstRunState(state) {
|
|
59
|
+
ensureUserDir();
|
|
60
|
+
state.updatedAt = new Date().toISOString();
|
|
61
|
+
fs_1.default.writeFileSync(getStatePath(), JSON.stringify(state, null, 2));
|
|
62
|
+
}
|
|
63
|
+
function clearFirstRunState() {
|
|
64
|
+
const statePath = getStatePath();
|
|
65
|
+
if (fs_1.default.existsSync(statePath)) {
|
|
66
|
+
fs_1.default.unlinkSync(statePath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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.FirstRunServer = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
function resolveFirstRunPublicDir() {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path_1.default.resolve(process.cwd(), 'public/first-run'),
|
|
14
|
+
path_1.default.resolve(__dirname, '..', '..', 'public/first-run'),
|
|
15
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'public/first-run'),
|
|
16
|
+
];
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Could not locate public/first-run assets.');
|
|
23
|
+
}
|
|
24
|
+
function pickProjectPath() {
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
const script = [
|
|
27
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
28
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
29
|
+
'$dialog.ShowNewFolderButton = $true',
|
|
30
|
+
'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
31
|
+
' Write-Output $dialog.SelectedPath',
|
|
32
|
+
'}',
|
|
33
|
+
].join('; ');
|
|
34
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-Command', script], {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
});
|
|
37
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
38
|
+
}
|
|
39
|
+
if (process.platform === 'darwin') {
|
|
40
|
+
const result = (0, child_process_1.spawnSync)('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select a FRAIM project folder")'], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
});
|
|
43
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
44
|
+
}
|
|
45
|
+
const result = (0, child_process_1.spawnSync)('bash', ['-lc', 'zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null'], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
});
|
|
48
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
49
|
+
}
|
|
50
|
+
class FirstRunServer {
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.app = (0, express_1.default)();
|
|
53
|
+
this.sessionService = options.sessionService;
|
|
54
|
+
this.finishPromise = new Promise((resolve) => {
|
|
55
|
+
this.finishResolver = resolve;
|
|
56
|
+
});
|
|
57
|
+
this.app.use(express_1.default.json());
|
|
58
|
+
this.app.use('/api/first-run', (req, res, next) => {
|
|
59
|
+
if (req.method === 'GET') {
|
|
60
|
+
return next();
|
|
61
|
+
}
|
|
62
|
+
const requestToken = req.header('x-fraim-first-run-token');
|
|
63
|
+
if (requestToken !== this.sessionService.getRequestToken()) {
|
|
64
|
+
return res.status(403).json({ error: 'Invalid first-run session token.' });
|
|
65
|
+
}
|
|
66
|
+
return next();
|
|
67
|
+
});
|
|
68
|
+
this.app.use('/first-run', express_1.default.static(resolveFirstRunPublicDir()));
|
|
69
|
+
this.app.get('/health', (_req, res) => {
|
|
70
|
+
res.json({ status: 'ok', service: 'fraim-first-run' });
|
|
71
|
+
});
|
|
72
|
+
this.registerRoutes();
|
|
73
|
+
}
|
|
74
|
+
async start(port) {
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
this.httpServer = this.app.listen(port, '127.0.0.1');
|
|
77
|
+
this.httpServer.once('listening', () => resolve());
|
|
78
|
+
this.httpServer.once('error', (error) => reject(error));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async waitForFinish() {
|
|
82
|
+
await this.finishPromise;
|
|
83
|
+
}
|
|
84
|
+
async stop() {
|
|
85
|
+
if (!this.httpServer) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
this.httpServer.close((error) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
this.httpServer = undefined;
|
|
98
|
+
}
|
|
99
|
+
registerRoutes() {
|
|
100
|
+
this.app.get('/api/first-run/session', (_req, res) => {
|
|
101
|
+
res.json(this.sessionService.getSession());
|
|
102
|
+
});
|
|
103
|
+
this.app.post('/api/first-run/prereqs', (_req, res) => {
|
|
104
|
+
res.json(this.sessionService.runPrereqChecks());
|
|
105
|
+
});
|
|
106
|
+
this.app.post('/api/first-run/agents/select', (req, res) => {
|
|
107
|
+
if (!req.body.agentId) {
|
|
108
|
+
return res.status(400).json({ error: 'agentId is required.' });
|
|
109
|
+
}
|
|
110
|
+
return res.json(this.sessionService.selectAgent(req.body.agentId));
|
|
111
|
+
});
|
|
112
|
+
this.app.post('/api/first-run/configure', async (_req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
return res.json(await this.sessionService.configureFraim());
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not configure FRAIM.' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
this.app.post('/api/first-run/project-path/pick', (_req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const selectedPath = pickProjectPath();
|
|
123
|
+
if (!selectedPath) {
|
|
124
|
+
return res.status(204).end();
|
|
125
|
+
}
|
|
126
|
+
return res.json({ path: selectedPath });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open the folder picker.' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
this.app.post('/api/first-run/project', async (req, res) => {
|
|
133
|
+
if (!req.body.projectPath) {
|
|
134
|
+
return res.status(400).json({ error: 'projectPath is required.' });
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return res.json(await this.sessionService.initializeProject(req.body.projectPath, req.body.initializeGit !== false));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not initialize the project.' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
this.app.post('/api/first-run/launch', (_req, res) => {
|
|
144
|
+
return res.json(this.sessionService.launchAndProbe());
|
|
145
|
+
});
|
|
146
|
+
this.app.post('/api/first-run/finish', (_req, res) => {
|
|
147
|
+
const result = this.sessionService.finish();
|
|
148
|
+
this.finishResolver?.();
|
|
149
|
+
return res.json(result);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.FirstRunServer = FirstRunServer;
|
|
@@ -0,0 +1,302 @@
|
|
|
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.FirstRunSessionService = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const ide_detector_1 = require("../cli/setup/ide-detector");
|
|
13
|
+
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
14
|
+
const setup_1 = require("../cli/commands/setup");
|
|
15
|
+
const init_project_1 = require("../cli/commands/init-project");
|
|
16
|
+
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
const install_state_1 = require("./install-state");
|
|
19
|
+
function commandExists(command) {
|
|
20
|
+
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
21
|
+
const args = process.platform === 'win32'
|
|
22
|
+
? ['/d', '/s', '/c', `${command} --version`]
|
|
23
|
+
: ['--version'];
|
|
24
|
+
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
27
|
+
shell: false,
|
|
28
|
+
});
|
|
29
|
+
return result.status === 0;
|
|
30
|
+
}
|
|
31
|
+
function ensureOutputDirs() {
|
|
32
|
+
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
33
|
+
fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
function getNextPromptPath() {
|
|
36
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install', 'next-prompt.txt');
|
|
37
|
+
}
|
|
38
|
+
function getInstallLogPath() {
|
|
39
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-log.txt');
|
|
40
|
+
}
|
|
41
|
+
function appendInstallLog(line) {
|
|
42
|
+
ensureOutputDirs();
|
|
43
|
+
fs_1.default.appendFileSync(getInstallLogPath(), `[${new Date().toISOString()}] ${line}${os_1.default.EOL}`);
|
|
44
|
+
}
|
|
45
|
+
function getLaunchInstruction(agentId, projectRoot) {
|
|
46
|
+
const cwd = projectRoot || process.cwd();
|
|
47
|
+
switch (agentId) {
|
|
48
|
+
case 'claude-code':
|
|
49
|
+
return `cd "${cwd}" && claude`;
|
|
50
|
+
case 'codex':
|
|
51
|
+
return `cd "${cwd}" && codex`;
|
|
52
|
+
case 'gemini-cli':
|
|
53
|
+
return `cd "${cwd}" && gemini`;
|
|
54
|
+
default:
|
|
55
|
+
return `cd "${cwd}"`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function maybeLaunchInteractiveShell(agentId, projectRoot) {
|
|
59
|
+
const commandLine = getLaunchInstruction(agentId, projectRoot);
|
|
60
|
+
try {
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
(0, child_process_1.spawn)('cmd.exe', ['/d', '/s', '/c', `start "" cmd /k ${commandLine}`], {
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: 'ignore',
|
|
65
|
+
}).unref();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (process.platform === 'darwin') {
|
|
69
|
+
(0, child_process_1.spawn)('open', ['-a', 'Terminal', projectRoot], {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: 'ignore',
|
|
72
|
+
}).unref();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
(0, child_process_1.spawn)('x-terminal-emulator', ['-e', 'bash', '-lc', commandLine], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: 'ignore',
|
|
78
|
+
}).unref();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function probeCodex(projectRoot) {
|
|
86
|
+
const result = (0, child_process_1.spawnSync)('codex', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'], {
|
|
87
|
+
cwd: projectRoot,
|
|
88
|
+
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
timeout: 60000,
|
|
91
|
+
});
|
|
92
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
93
|
+
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
94
|
+
}
|
|
95
|
+
function probeClaude(projectRoot) {
|
|
96
|
+
const result = (0, child_process_1.spawnSync)('claude', ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'], {
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
timeout: 60000,
|
|
101
|
+
});
|
|
102
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
103
|
+
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
104
|
+
}
|
|
105
|
+
function probeGemini(projectRoot) {
|
|
106
|
+
const result = (0, child_process_1.spawnSync)('gemini', ['--help'], {
|
|
107
|
+
cwd: projectRoot,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
timeout: 30000,
|
|
110
|
+
});
|
|
111
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
112
|
+
return {
|
|
113
|
+
ok: result.status === 0,
|
|
114
|
+
output: output || 'Gemini CLI responded to --help; runtime FRAIM probe still requires manual confirmation.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
class FirstRunSessionService {
|
|
118
|
+
constructor(options) {
|
|
119
|
+
this.key = options.key;
|
|
120
|
+
this.headless = options.headless === true;
|
|
121
|
+
this.fakeMode = process.env.FRAIM_FIRST_RUN_FAKE === '1';
|
|
122
|
+
this.requestToken = crypto_1.default.randomUUID();
|
|
123
|
+
this.state = options.resume ? (0, install_state_1.loadFirstRunState)() || (0, install_state_1.createInitialFirstRunState)(options.key) : (0, install_state_1.createInitialFirstRunState)(options.key);
|
|
124
|
+
if (options.projectRoot) {
|
|
125
|
+
this.state.workspacePath = path_1.default.resolve(options.projectRoot);
|
|
126
|
+
this.state.stepStates.project = 'running';
|
|
127
|
+
(0, install_state_1.saveFirstRunState)(this.state);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
persist() {
|
|
131
|
+
(0, install_state_1.saveFirstRunState)(this.state);
|
|
132
|
+
}
|
|
133
|
+
markStep(step, status) {
|
|
134
|
+
this.state.stepStates[step] = status;
|
|
135
|
+
this.persist();
|
|
136
|
+
}
|
|
137
|
+
detectAgents() {
|
|
138
|
+
if (this.fakeMode) {
|
|
139
|
+
return types_1.FIRST_RUN_AGENT_OPTIONS.map((agent) => ({
|
|
140
|
+
id: agent.id,
|
|
141
|
+
label: agent.label,
|
|
142
|
+
detected: true,
|
|
143
|
+
detail: 'Fake test-mode agent available.',
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
const detected = (0, ide_detector_1.detectInstalledIDEs)();
|
|
147
|
+
return types_1.FIRST_RUN_AGENT_OPTIONS.map((agent) => {
|
|
148
|
+
const ide = detected.find((entry) => entry.aliases?.some((alias) => agent.detectAliases.includes(alias)) ||
|
|
149
|
+
agent.detectAliases.some((alias) => entry.name.toLowerCase().includes(alias)));
|
|
150
|
+
return {
|
|
151
|
+
id: agent.id,
|
|
152
|
+
label: agent.label,
|
|
153
|
+
detected: Boolean(ide) || commandExists(agent.launchCommand),
|
|
154
|
+
detail: ide ? `Config path: ${ide.configPath}` : 'CLI/config not detected yet.',
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
getRequestToken() {
|
|
159
|
+
return this.requestToken;
|
|
160
|
+
}
|
|
161
|
+
getSession() {
|
|
162
|
+
const agents = this.detectAgents();
|
|
163
|
+
this.state.detectedAgents = agents.filter((agent) => agent.detected).map((agent) => agent.id);
|
|
164
|
+
this.persist();
|
|
165
|
+
return {
|
|
166
|
+
state: this.state,
|
|
167
|
+
agents,
|
|
168
|
+
prompt: types_1.FIRST_RUN_PROMPT,
|
|
169
|
+
headless: this.headless,
|
|
170
|
+
requestToken: this.requestToken,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
runPrereqChecks() {
|
|
174
|
+
this.markStep('prereqs', 'running');
|
|
175
|
+
const checks = [
|
|
176
|
+
{ label: 'node', ok: commandExists('node') },
|
|
177
|
+
{ label: 'npx', ok: commandExists('npx') },
|
|
178
|
+
{ label: 'git', ok: commandExists('git') },
|
|
179
|
+
];
|
|
180
|
+
const failures = checks.filter((check) => !check.ok).map((check) => check.label);
|
|
181
|
+
const message = failures.length === 0
|
|
182
|
+
? 'Node.js, npx, and git are available.'
|
|
183
|
+
: `Missing prerequisites: ${failures.join(', ')}.`;
|
|
184
|
+
appendInstallLog(`prereq-check ${message}`);
|
|
185
|
+
this.markStep('prereqs', failures.length === 0 ? 'complete' : 'failed');
|
|
186
|
+
if (failures.length === 0) {
|
|
187
|
+
this.markStep('agent', 'running');
|
|
188
|
+
}
|
|
189
|
+
return { ok: failures.length === 0, message, state: this.state };
|
|
190
|
+
}
|
|
191
|
+
selectAgent(agentId) {
|
|
192
|
+
this.state.selectedAgentId = agentId;
|
|
193
|
+
this.markStep('agent', 'complete');
|
|
194
|
+
this.markStep('configure', 'running');
|
|
195
|
+
appendInstallLog(`agent-selected ${agentId}`);
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
message: `Selected ${types_1.FIRST_RUN_AGENT_OPTIONS.find((agent) => agent.id === agentId)?.label || agentId}.`,
|
|
199
|
+
state: this.state,
|
|
200
|
+
launchCommand: types_1.FIRST_RUN_AGENT_OPTIONS.find((agent) => agent.id === agentId)?.loginCommand,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async configureFraim() {
|
|
204
|
+
this.markStep('configure', 'running');
|
|
205
|
+
(0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
|
|
206
|
+
if (!this.fakeMode) {
|
|
207
|
+
const selectedAgent = this.state.selectedAgentId ? (0, ide_detector_1.findIDEByName)(this.state.selectedAgentId) : undefined;
|
|
208
|
+
const targetNames = selectedAgent ? [selectedAgent.name] : undefined;
|
|
209
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, targetNames, {});
|
|
210
|
+
this.state.configuredAgents = this.detectAgents().filter((agent) => agent.detected).map((agent) => agent.id);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.state.configuredAgents = this.state.selectedAgentId ? [this.state.selectedAgentId] : [];
|
|
214
|
+
}
|
|
215
|
+
appendInstallLog(`configure configuredAgents=${this.state.configuredAgents.join(',')}`);
|
|
216
|
+
this.markStep('configure', 'complete');
|
|
217
|
+
this.markStep('project', 'running');
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
message: 'Saved FRAIM global configuration and wrote agent config where available.',
|
|
221
|
+
state: this.state,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async initializeProject(projectPath, initializeGit = true) {
|
|
225
|
+
const resolvedPath = path_1.default.resolve(projectPath);
|
|
226
|
+
fs_1.default.mkdirSync(resolvedPath, { recursive: true });
|
|
227
|
+
this.markStep('project', 'running');
|
|
228
|
+
if (initializeGit && commandExists('git')) {
|
|
229
|
+
try {
|
|
230
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { cwd: resolvedPath, stdio: 'ignore' });
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
(0, child_process_1.execSync)('git init', { cwd: resolvedPath, stdio: 'ignore' });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
await (0, init_project_1.runInitProject)({ projectRoot: resolvedPath });
|
|
237
|
+
this.state.workspacePath = resolvedPath;
|
|
238
|
+
this.markStep('project', 'complete');
|
|
239
|
+
this.markStep('launch', 'running');
|
|
240
|
+
appendInstallLog(`project-initialized ${resolvedPath}`);
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
message: `Initialized FRAIM in ${resolvedPath}.`,
|
|
244
|
+
state: this.state,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
launchAndProbe() {
|
|
248
|
+
const agentId = this.state.selectedAgentId;
|
|
249
|
+
const projectRoot = this.state.workspacePath;
|
|
250
|
+
if (!agentId || !projectRoot) {
|
|
251
|
+
this.markStep('launch', 'failed');
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
message: 'Select an agent and project folder before launch.',
|
|
255
|
+
state: this.state,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const launchCommand = getLaunchInstruction(agentId, projectRoot);
|
|
259
|
+
const opened = this.fakeMode ? true : maybeLaunchInteractiveShell(agentId, projectRoot);
|
|
260
|
+
let probeResult = { ok: true, output: 'Fake test-mode launch succeeded.' };
|
|
261
|
+
if (!this.fakeMode) {
|
|
262
|
+
if (agentId === 'codex') {
|
|
263
|
+
probeResult = probeCodex(projectRoot);
|
|
264
|
+
}
|
|
265
|
+
else if (agentId === 'claude-code') {
|
|
266
|
+
probeResult = probeClaude(projectRoot);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
probeResult = probeGemini(projectRoot);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.state.lastLaunchCommand = launchCommand;
|
|
273
|
+
this.state.lastProbeMessage = probeResult.output;
|
|
274
|
+
this.markStep('launch', probeResult.ok ? 'complete' : 'failed');
|
|
275
|
+
if (probeResult.ok) {
|
|
276
|
+
this.markStep('finish', 'running');
|
|
277
|
+
}
|
|
278
|
+
appendInstallLog(`launch agent=${agentId} opened=${opened} probeOk=${probeResult.ok}`);
|
|
279
|
+
return {
|
|
280
|
+
ok: probeResult.ok,
|
|
281
|
+
message: opened
|
|
282
|
+
? 'Agent launch was attempted and the probe completed.'
|
|
283
|
+
: 'Agent launch could not be opened automatically; use the provided command and review the probe output.',
|
|
284
|
+
state: this.state,
|
|
285
|
+
launchCommand,
|
|
286
|
+
output: probeResult.output,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
finish() {
|
|
290
|
+
ensureOutputDirs();
|
|
291
|
+
fs_1.default.writeFileSync(getNextPromptPath(), `${types_1.FIRST_RUN_PROMPT}${os_1.default.EOL}`, 'utf8');
|
|
292
|
+
this.state.nextPrompt = types_1.FIRST_RUN_PROMPT;
|
|
293
|
+
this.markStep('finish', 'complete');
|
|
294
|
+
appendInstallLog('finish prompt-written');
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
message: 'Wrote the next prompt artifact and completed first-run.',
|
|
298
|
+
state: this.state,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
exports.FirstRunSessionService = FirstRunSessionService;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FIRST_RUN_AGENT_OPTIONS = exports.FIRST_RUN_RESOURCES_URL = exports.FIRST_RUN_PROMPT = void 0;
|
|
4
|
+
exports.createDefaultStepStates = createDefaultStepStates;
|
|
5
|
+
exports.FIRST_RUN_PROMPT = 'Onboard this project';
|
|
6
|
+
exports.FIRST_RUN_RESOURCES_URL = 'https://fraimworks.ai/resources.html';
|
|
7
|
+
exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
id: 'claude-code',
|
|
10
|
+
label: 'Claude Code',
|
|
11
|
+
detectAliases: ['claude-code', 'claude code', 'claude'],
|
|
12
|
+
loginCommand: 'claude',
|
|
13
|
+
launchCommand: 'claude',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'codex',
|
|
17
|
+
label: 'Codex',
|
|
18
|
+
detectAliases: ['codex'],
|
|
19
|
+
loginCommand: 'codex login',
|
|
20
|
+
launchCommand: 'codex',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'gemini-cli',
|
|
24
|
+
label: 'Gemini CLI',
|
|
25
|
+
detectAliases: ['gemini-cli', 'gemini cli', 'gemini'],
|
|
26
|
+
loginCommand: 'gemini',
|
|
27
|
+
launchCommand: 'gemini',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
function createDefaultStepStates() {
|
|
31
|
+
return {
|
|
32
|
+
welcome: 'pending',
|
|
33
|
+
prereqs: 'pending',
|
|
34
|
+
agent: 'pending',
|
|
35
|
+
configure: 'pending',
|
|
36
|
+
project: 'pending',
|
|
37
|
+
launch: 'pending',
|
|
38
|
+
finish: 'pending',
|
|
39
|
+
};
|
|
40
|
+
}
|