fraim-framework 2.0.122 ā 2.0.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ai-hub/catalog.js +131 -0
- package/dist/src/ai-hub/desktop-main.js +111 -0
- package/dist/src/ai-hub/hosts.js +241 -0
- package/dist/src/ai-hub/preferences.js +55 -0
- package/dist/src/ai-hub/server.js +307 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/cli/commands/hub.js +96 -0
- package/dist/src/cli/commands/setup.js +5 -5
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/user-level-sync.js +59 -0
- package/dist/src/core/quality-evidence.js +67 -36
- package/dist/src/local-mcp-server/stdio-server.js +10 -1
- package/package.json +150 -146
- package/public/ai-hub/index.html +130 -0
- package/public/ai-hub/script.js +374 -0
- package/public/ai-hub/styles.css +568 -0
|
@@ -0,0 +1,307 @@
|
|
|
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.AiHubServer = void 0;
|
|
7
|
+
exports.findAvailablePort = findAvailablePort;
|
|
8
|
+
const express_1 = __importDefault(require("express"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const net_1 = __importDefault(require("net"));
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const catalog_1 = require("./catalog");
|
|
15
|
+
const hosts_1 = require("./hosts");
|
|
16
|
+
const preferences_1 = require("./preferences");
|
|
17
|
+
class AiHubRunRegistry {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.runs = new Map();
|
|
20
|
+
this.children = new Map();
|
|
21
|
+
}
|
|
22
|
+
listLatest() {
|
|
23
|
+
return [...this.runs.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
24
|
+
}
|
|
25
|
+
get(runId) {
|
|
26
|
+
return this.runs.get(runId);
|
|
27
|
+
}
|
|
28
|
+
create(run, child) {
|
|
29
|
+
this.runs.set(run.id, run);
|
|
30
|
+
this.children.set(run.id, child);
|
|
31
|
+
return run;
|
|
32
|
+
}
|
|
33
|
+
update(runId, updater) {
|
|
34
|
+
const run = this.runs.get(runId);
|
|
35
|
+
if (!run) {
|
|
36
|
+
throw new Error(`Run ${runId} not found`);
|
|
37
|
+
}
|
|
38
|
+
updater(run);
|
|
39
|
+
run.updatedAt = new Date().toISOString();
|
|
40
|
+
return run;
|
|
41
|
+
}
|
|
42
|
+
dispose(runId) {
|
|
43
|
+
this.children.delete(runId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function ensureDirectoryPath(projectPath) {
|
|
47
|
+
const trimmed = (projectPath || '').trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
throw new Error('Project path is required.');
|
|
50
|
+
}
|
|
51
|
+
const resolved = path_1.default.resolve(trimmed);
|
|
52
|
+
const stat = fs_1.default.existsSync(resolved) ? fs_1.default.statSync(resolved) : null;
|
|
53
|
+
if (!stat || !stat.isDirectory()) {
|
|
54
|
+
throw new Error('Project path must point to an existing directory.');
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
class AiHubServer {
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.app = (0, express_1.default)();
|
|
61
|
+
this.runRegistry = new AiHubRunRegistry();
|
|
62
|
+
this.projectPath = options.projectPath || process.cwd();
|
|
63
|
+
this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
|
|
64
|
+
this.hostRuntime = options.hostRuntime || (process.env.FRAIM_AI_HUB_FAKE_HOST === '1' ? new hosts_1.FakeHostRuntime() : new hosts_1.CliHostRuntime());
|
|
65
|
+
this.app.use(express_1.default.json());
|
|
66
|
+
this.app.use('/ai-hub', express_1.default.static(resolveAiHubPublicDir()));
|
|
67
|
+
this.app.get('/health', (_req, res) => {
|
|
68
|
+
res.json({ status: 'ok', service: 'fraim-ai-hub' });
|
|
69
|
+
});
|
|
70
|
+
this.registerRoutes();
|
|
71
|
+
}
|
|
72
|
+
getApp() {
|
|
73
|
+
return this.app;
|
|
74
|
+
}
|
|
75
|
+
async start(port) {
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
this.httpServer = this.app.listen(port, '127.0.0.1');
|
|
78
|
+
this.httpServer.once('listening', () => resolve());
|
|
79
|
+
this.httpServer.once('error', (error) => reject(error));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async stop() {
|
|
83
|
+
if (!this.httpServer)
|
|
84
|
+
return;
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
this.httpServer.close((error) => {
|
|
87
|
+
if (error) {
|
|
88
|
+
reject(error);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
this.httpServer = undefined;
|
|
95
|
+
}
|
|
96
|
+
bootstrapResponse(projectPath) {
|
|
97
|
+
const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
|
|
98
|
+
const preferences = this.preferencesStore.load(normalizedProjectPath);
|
|
99
|
+
const project = (0, catalog_1.summarizeProject)(normalizedProjectPath);
|
|
100
|
+
const jobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath);
|
|
101
|
+
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath);
|
|
102
|
+
return {
|
|
103
|
+
title: 'Visa AI Hub',
|
|
104
|
+
project,
|
|
105
|
+
preferences,
|
|
106
|
+
categories: (0, catalog_1.getAiHubCategories)(),
|
|
107
|
+
jobs,
|
|
108
|
+
managerTemplates,
|
|
109
|
+
employees: this.hostRuntime.detectEmployees(),
|
|
110
|
+
activeRun: this.runRegistry.listLatest(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
registerRoutes() {
|
|
114
|
+
this.app.get('/api/ai-hub/bootstrap', (req, res) => {
|
|
115
|
+
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
|
|
116
|
+
? req.query.projectPath
|
|
117
|
+
: this.projectPath;
|
|
118
|
+
res.json(this.bootstrapResponse(projectPath));
|
|
119
|
+
});
|
|
120
|
+
this.app.post('/api/ai-hub/project-path/pick', (_req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const projectPath = pickProjectPath();
|
|
123
|
+
if (!projectPath) {
|
|
124
|
+
return res.status(204).end();
|
|
125
|
+
}
|
|
126
|
+
return res.json({ path: projectPath });
|
|
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/ai-hub/runs', (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const projectPath = ensureDirectoryPath(req.body.projectPath || this.projectPath);
|
|
135
|
+
const hostId = req.body.hostId;
|
|
136
|
+
const jobId = req.body.jobId;
|
|
137
|
+
const message = (req.body.message || '').trim();
|
|
138
|
+
if (hostId !== 'codex' && hostId !== 'claude') {
|
|
139
|
+
throw new Error('Choose an available employee before starting a job.');
|
|
140
|
+
}
|
|
141
|
+
if (!jobId) {
|
|
142
|
+
throw new Error('Choose a FRAIM job before starting a run.');
|
|
143
|
+
}
|
|
144
|
+
if (!message) {
|
|
145
|
+
throw new Error('Coach your employee before starting the run.');
|
|
146
|
+
}
|
|
147
|
+
const employee = this.hostRuntime.detectEmployees().find((entry) => entry.id === hostId);
|
|
148
|
+
if (!employee?.available) {
|
|
149
|
+
throw new Error(`${employee?.label || 'Selected employee'} is not available on this machine.`);
|
|
150
|
+
}
|
|
151
|
+
const run = {
|
|
152
|
+
id: (0, crypto_1.randomUUID)(),
|
|
153
|
+
jobId,
|
|
154
|
+
hostId,
|
|
155
|
+
projectPath,
|
|
156
|
+
status: 'running',
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
updatedAt: new Date().toISOString(),
|
|
159
|
+
messages: [(0, hosts_1.createHubMessage)('manager', message)],
|
|
160
|
+
events: [(0, hosts_1.createHubEvent)('system', `Starting ${hostId} in ${projectPath}`)],
|
|
161
|
+
};
|
|
162
|
+
this.runRegistry.create(run, {});
|
|
163
|
+
const child = this.hostRuntime.startRun(hostId, projectPath, message, {
|
|
164
|
+
onEvent: (event, channel) => {
|
|
165
|
+
this.runRegistry.update(run.id, (current) => {
|
|
166
|
+
if (event.sessionId)
|
|
167
|
+
current.sessionId = event.sessionId;
|
|
168
|
+
if (event.message)
|
|
169
|
+
current.messages.push((0, hosts_1.createHubMessage)('employee', event.message));
|
|
170
|
+
if (event.raw)
|
|
171
|
+
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
onExit: (exitCode) => {
|
|
175
|
+
this.runRegistry.update(run.id, (current) => {
|
|
176
|
+
current.exitCode = exitCode;
|
|
177
|
+
current.status = exitCode === 0 ? 'completed' : 'failed';
|
|
178
|
+
current.events.push((0, hosts_1.createHubEvent)('system', `Run exited with code ${exitCode ?? 'unknown'}.`));
|
|
179
|
+
});
|
|
180
|
+
this.runRegistry.dispose(run.id);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
this.runRegistry.create(run, child);
|
|
184
|
+
const existingPreferences = this.preferencesStore.load(projectPath);
|
|
185
|
+
this.preferencesStore.remember({
|
|
186
|
+
...existingPreferences,
|
|
187
|
+
projectPath,
|
|
188
|
+
employeeId: hostId,
|
|
189
|
+
recentJobIds: existingPreferences.recentJobIds,
|
|
190
|
+
}, jobId);
|
|
191
|
+
res.status(201).json(run);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
res.status(400).json({ error: error instanceof Error ? error.message : 'Could not start run.' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this.app.post('/api/ai-hub/runs/:runId/messages', (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const run = this.runRegistry.get(req.params.runId);
|
|
200
|
+
if (!run) {
|
|
201
|
+
return res.status(404).json({ error: 'Run not found.' });
|
|
202
|
+
}
|
|
203
|
+
if (!run.sessionId) {
|
|
204
|
+
return res.status(409).json({ error: 'This run does not have a resumable host session yet.' });
|
|
205
|
+
}
|
|
206
|
+
const message = (req.body.message || '').trim();
|
|
207
|
+
if (!message) {
|
|
208
|
+
return res.status(400).json({ error: 'Coach your employee before sending the next turn.' });
|
|
209
|
+
}
|
|
210
|
+
this.runRegistry.update(run.id, (current) => {
|
|
211
|
+
current.status = 'running';
|
|
212
|
+
current.messages.push((0, hosts_1.createHubMessage)('manager', message));
|
|
213
|
+
});
|
|
214
|
+
this.runRegistry.create(run, {});
|
|
215
|
+
const child = this.hostRuntime.continueRun(run.hostId, run.projectPath, run.sessionId, message, {
|
|
216
|
+
onEvent: (event, channel) => {
|
|
217
|
+
this.runRegistry.update(run.id, (current) => {
|
|
218
|
+
if (event.sessionId)
|
|
219
|
+
current.sessionId = event.sessionId;
|
|
220
|
+
if (event.message)
|
|
221
|
+
current.messages.push((0, hosts_1.createHubMessage)('employee', event.message));
|
|
222
|
+
if (event.raw)
|
|
223
|
+
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
onExit: (exitCode) => {
|
|
227
|
+
this.runRegistry.update(run.id, (current) => {
|
|
228
|
+
current.exitCode = exitCode;
|
|
229
|
+
current.status = exitCode === 0 ? 'completed' : 'failed';
|
|
230
|
+
current.events.push((0, hosts_1.createHubEvent)('system', `Run exited with code ${exitCode ?? 'unknown'}.`));
|
|
231
|
+
});
|
|
232
|
+
this.runRegistry.dispose(run.id);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
this.runRegistry.create(run, child);
|
|
236
|
+
res.json(this.runRegistry.get(run.id));
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
res.status(400).json({ error: error instanceof Error ? error.message : 'Could not continue run.' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
this.app.get('/api/ai-hub/runs/:runId', (req, res) => {
|
|
243
|
+
const run = this.runRegistry.get(req.params.runId);
|
|
244
|
+
if (!run) {
|
|
245
|
+
return res.status(404).json({ error: 'Run not found.' });
|
|
246
|
+
}
|
|
247
|
+
return res.json(run);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
exports.AiHubServer = AiHubServer;
|
|
252
|
+
async function findAvailablePort(preferredPort) {
|
|
253
|
+
let port = preferredPort;
|
|
254
|
+
while (port < preferredPort + 20) {
|
|
255
|
+
const available = await new Promise((resolve) => {
|
|
256
|
+
const server = net_1.default.createServer();
|
|
257
|
+
server.once('error', () => resolve(false));
|
|
258
|
+
server.once('listening', () => {
|
|
259
|
+
server.close(() => resolve(true));
|
|
260
|
+
});
|
|
261
|
+
server.listen(port, '127.0.0.1');
|
|
262
|
+
});
|
|
263
|
+
if (available)
|
|
264
|
+
return port;
|
|
265
|
+
port += 1;
|
|
266
|
+
}
|
|
267
|
+
throw new Error(`No free loopback port found near ${preferredPort}.`);
|
|
268
|
+
}
|
|
269
|
+
function resolveAiHubPublicDir() {
|
|
270
|
+
const candidates = [
|
|
271
|
+
path_1.default.resolve(process.cwd(), 'public/ai-hub'),
|
|
272
|
+
path_1.default.resolve(__dirname, '..', '..', 'public/ai-hub'),
|
|
273
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'public/ai-hub'),
|
|
274
|
+
];
|
|
275
|
+
for (const candidate of candidates) {
|
|
276
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
277
|
+
return candidate;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
throw new Error('Could not locate public/ai-hub assets.');
|
|
281
|
+
}
|
|
282
|
+
function pickProjectPath() {
|
|
283
|
+
if (process.platform === 'win32') {
|
|
284
|
+
const script = [
|
|
285
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
286
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
287
|
+
'$dialog.ShowNewFolderButton = $false',
|
|
288
|
+
'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
289
|
+
' Write-Output $dialog.SelectedPath',
|
|
290
|
+
'}',
|
|
291
|
+
].join('; ');
|
|
292
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-Command', script], {
|
|
293
|
+
encoding: 'utf8',
|
|
294
|
+
});
|
|
295
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
296
|
+
}
|
|
297
|
+
if (process.platform === 'darwin') {
|
|
298
|
+
const result = (0, child_process_1.spawnSync)('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select a FRAIM project folder")'], {
|
|
299
|
+
encoding: 'utf8',
|
|
300
|
+
});
|
|
301
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
302
|
+
}
|
|
303
|
+
const result = (0, child_process_1.spawnSync)('bash', ['-lc', 'zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null'], {
|
|
304
|
+
encoding: 'utf8',
|
|
305
|
+
});
|
|
306
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
307
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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.hubCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const server_1 = require("../../ai-hub/server");
|
|
9
|
+
const git_utils_1 = require("../../core/utils/git-utils");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
function resolveElectronBinary() {
|
|
14
|
+
try {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
16
|
+
return require('electron');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function resolveDesktopEntry() {
|
|
23
|
+
const candidates = [
|
|
24
|
+
path_1.default.resolve(__dirname, '..', '..', 'ai-hub', 'desktop-main.js'),
|
|
25
|
+
path_1.default.resolve(process.cwd(), 'dist', 'src', 'ai-hub', 'desktop-main.js'),
|
|
26
|
+
];
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
fs_1.default.accessSync(candidate);
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function openDesktopWindow(projectPath, preferredPort) {
|
|
39
|
+
const electronBinary = resolveElectronBinary();
|
|
40
|
+
const desktopEntry = resolveDesktopEntry();
|
|
41
|
+
if (!electronBinary || !desktopEntry) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const child = (0, child_process_1.spawn)(electronBinary, [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)], {
|
|
45
|
+
detached: true,
|
|
46
|
+
stdio: 'ignore',
|
|
47
|
+
});
|
|
48
|
+
child.unref();
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function openBrowser(url) {
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const child = (0, child_process_1.spawn)('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
|
|
54
|
+
child.unref();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === 'darwin') {
|
|
58
|
+
const child = (0, child_process_1.spawn)('open', [url], { detached: true, stdio: 'ignore' });
|
|
59
|
+
child.unref();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const child = (0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
63
|
+
child.unref();
|
|
64
|
+
}
|
|
65
|
+
exports.hubCommand = new commander_1.Command('hub')
|
|
66
|
+
.description('Start the Visa AI Hub local companion for running FRAIM jobs through Codex or Claude Code')
|
|
67
|
+
.option('--port <port>', 'Preferred local port for the hub', (value) => Number(value), 43091)
|
|
68
|
+
.option('--project-path <path>', 'Initial project path for job discovery', process.cwd())
|
|
69
|
+
.option('--no-open', 'Do not open the hub after startup')
|
|
70
|
+
.option('--browser', 'Open in the default browser instead of the desktop shell')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
|
|
73
|
+
const projectPath = path_1.default.resolve(options.projectPath || process.cwd());
|
|
74
|
+
if (options.open) {
|
|
75
|
+
const openedDesktop = !options.browser && openDesktopWindow(projectPath, preferredPort);
|
|
76
|
+
if (!openedDesktop) {
|
|
77
|
+
const port = await (0, server_1.findAvailablePort)(preferredPort);
|
|
78
|
+
const server = new server_1.AiHubServer({ projectPath });
|
|
79
|
+
await server.start(port);
|
|
80
|
+
const url = `http://127.0.0.1:${port}/ai-hub/`;
|
|
81
|
+
console.log(`Visa AI Hub running at ${url}`);
|
|
82
|
+
console.log(`Project path: ${projectPath}`);
|
|
83
|
+
openBrowser(url);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log('Visa AI Hub desktop shell launched.');
|
|
87
|
+
console.log(`Project path: ${projectPath}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const port = await (0, server_1.findAvailablePort)(preferredPort);
|
|
91
|
+
const server = new server_1.AiHubServer({ projectPath });
|
|
92
|
+
await server.start(port);
|
|
93
|
+
const url = `http://127.0.0.1:${port}/ai-hub/`;
|
|
94
|
+
console.log(`Visa AI Hub running at ${url}`);
|
|
95
|
+
console.log(`Project path: ${projectPath}`);
|
|
96
|
+
});
|
|
@@ -74,7 +74,7 @@ const promptForFraimKey = async () => {
|
|
|
74
74
|
process.exit(1);
|
|
75
75
|
}
|
|
76
76
|
console.log(chalk_1.default.blue('š FRAIM Key Setup'));
|
|
77
|
-
console.log('FRAIM requires a valid API key to access
|
|
77
|
+
console.log('FRAIM requires a valid API key to access jobs and features.\n');
|
|
78
78
|
let key = null;
|
|
79
79
|
let attempts = 0;
|
|
80
80
|
const maxAttempts = 3;
|
|
@@ -149,7 +149,7 @@ const promptForMode = async () => {
|
|
|
149
149
|
{
|
|
150
150
|
title: 'Conversational Mode',
|
|
151
151
|
value: 'conversational',
|
|
152
|
-
description: 'AI
|
|
152
|
+
description: 'AI jobs only, no platform integration required'
|
|
153
153
|
}
|
|
154
154
|
],
|
|
155
155
|
initial: 0
|
|
@@ -160,7 +160,7 @@ const promptForMode = async () => {
|
|
|
160
160
|
}
|
|
161
161
|
if (response.mode === 'conversational') {
|
|
162
162
|
console.log(chalk_1.default.blue('\nā Conversational mode selected'));
|
|
163
|
-
console.log(chalk_1.default.gray(' You can use FRAIM
|
|
163
|
+
console.log(chalk_1.default.gray(' You can use FRAIM jobs without platform credentials.'));
|
|
164
164
|
console.log(chalk_1.default.gray(' Platform features (issues, PRs) will be unavailable.\n'));
|
|
165
165
|
}
|
|
166
166
|
else if (response.mode === 'split') {
|
|
@@ -584,7 +584,7 @@ const runSetup = async (options) => {
|
|
|
584
584
|
});
|
|
585
585
|
if (mode === 'conversational' && Object.keys(mcpTokens).length === 0) {
|
|
586
586
|
console.log(chalk_1.default.yellow('ā¹ļø Conversational mode: Configuring MCP servers without platform integration'));
|
|
587
|
-
console.log(chalk_1.default.gray(' FRAIM
|
|
587
|
+
console.log(chalk_1.default.gray(' FRAIM jobs will work, but platform-specific features will be unavailable\n'));
|
|
588
588
|
}
|
|
589
589
|
try {
|
|
590
590
|
// Build providerConfigs map from configs
|
|
@@ -668,7 +668,7 @@ const runSetup = async (options) => {
|
|
|
668
668
|
console.log(chalk_1.default.white(' layer of your AI-powered work at once: AI agents become an'));
|
|
669
669
|
console.log(chalk_1.default.white(' accountable workforce, you become a capable AI manager, and'));
|
|
670
670
|
console.log(chalk_1.default.white(' your leadership gains clear optics on AI proficiency.'));
|
|
671
|
-
console.log(chalk_1.default.gray('\n
|
|
671
|
+
console.log(chalk_1.default.gray('\n 120+ jobs across engineering, marketing, fundraising,'));
|
|
672
672
|
console.log(chalk_1.default.gray(' legal, product, hiring, customer development, and more.'));
|
|
673
673
|
// Show which IDEs were configured and how to use FRAIM in each
|
|
674
674
|
const { detectInstalledIDEs: detectIDEs } = await Promise.resolve().then(() => __importStar(require('../setup/ide-detector')));
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -50,6 +50,7 @@ const list_overridable_1 = require("./commands/list-overridable");
|
|
|
50
50
|
const login_1 = require("./commands/login");
|
|
51
51
|
const mcp_1 = require("./commands/mcp");
|
|
52
52
|
const migrate_project_fraim_1 = require("./commands/migrate-project-fraim");
|
|
53
|
+
const hub_1 = require("./commands/hub");
|
|
53
54
|
const fs_1 = __importDefault(require("fs"));
|
|
54
55
|
const path_1 = __importDefault(require("path"));
|
|
55
56
|
const program = new commander_1.Command();
|
|
@@ -89,6 +90,7 @@ program.addCommand(list_overridable_1.listOverridableCommand);
|
|
|
89
90
|
program.addCommand(login_1.loginCommand);
|
|
90
91
|
program.addCommand(mcp_1.mcpCommand);
|
|
91
92
|
program.addCommand(migrate_project_fraim_1.migrateProjectFraimCommand);
|
|
93
|
+
program.addCommand(hub_1.hubCommand);
|
|
92
94
|
// Wait for async command initialization before parsing
|
|
93
95
|
(async () => {
|
|
94
96
|
// Import the initialization promise from setup command
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ensureUserLevelDirectories = ensureUserLevelDirectories;
|
|
7
|
+
exports.ensureUserLevelDependencies = ensureUserLevelDependencies;
|
|
7
8
|
exports.syncUserLevelArtifacts = syncUserLevelArtifacts;
|
|
8
9
|
/**
|
|
9
10
|
* User-Level FRAIM Setup
|
|
@@ -23,8 +24,19 @@ exports.syncUserLevelArtifacts = syncUserLevelArtifacts;
|
|
|
23
24
|
*/
|
|
24
25
|
const fs_1 = __importDefault(require("fs"));
|
|
25
26
|
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
26
28
|
const chalk_1 = __importDefault(require("chalk"));
|
|
27
29
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
30
|
+
/**
|
|
31
|
+
* Curated list of npm runtime dependencies that user-level scripts in
|
|
32
|
+
* ~/.fraim/scripts/ require at runtime. Keep this list narrow ā only add deps
|
|
33
|
+
* that are actually `require()`d by a script in registry/scripts/.
|
|
34
|
+
*
|
|
35
|
+
* Tracked in FRAIM #301 (author-audio first-run failure).
|
|
36
|
+
*/
|
|
37
|
+
const USER_LEVEL_RUNTIME_DEPS = {
|
|
38
|
+
'node-edge-tts': '*', // used by scripts/author-audio.js
|
|
39
|
+
};
|
|
28
40
|
/**
|
|
29
41
|
* Ensure the user-level FRAIM directory structure exists.
|
|
30
42
|
* Creates personalized-employee dirs for user-level overrides.
|
|
@@ -44,6 +56,52 @@ function ensureUserLevelDirectories(userFraimDir) {
|
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensure the curated set of npm runtime dependencies is installed under
|
|
61
|
+
* ~/.fraim/node_modules/. Maintains ~/.fraim/package.json so the deps survive
|
|
62
|
+
* a re-sync, and only runs `npm install` when something is actually missing.
|
|
63
|
+
*
|
|
64
|
+
* Soft-fails (yellow warning, no throw) if npm is unavailable or install
|
|
65
|
+
* errors ā setup must not break because a downstream script's dep couldn't
|
|
66
|
+
* be installed.
|
|
67
|
+
*/
|
|
68
|
+
function ensureUserLevelDependencies(userFraimDir) {
|
|
69
|
+
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
70
|
+
const pkgPath = path_1.default.join(baseDir, 'package.json');
|
|
71
|
+
const nodeModulesDir = path_1.default.join(baseDir, 'node_modules');
|
|
72
|
+
let pkg = {};
|
|
73
|
+
if (fs_1.default.existsSync(pkgPath)) {
|
|
74
|
+
try {
|
|
75
|
+
pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Corrupt package.json ā overwrite below rather than crash.
|
|
79
|
+
pkg = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
pkg.name = pkg.name || 'fraim-user-runtime';
|
|
83
|
+
pkg.private = true;
|
|
84
|
+
pkg.dependencies = { ...(pkg.dependencies || {}), ...USER_LEVEL_RUNTIME_DEPS };
|
|
85
|
+
fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
86
|
+
const missing = Object.keys(USER_LEVEL_RUNTIME_DEPS).filter((dep) => !fs_1.default.existsSync(path_1.default.join(nodeModulesDir, dep)));
|
|
87
|
+
if (missing.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk_1.default.blue(`š¦ Installing user-level runtime dependencies (${missing.join(', ')})...`));
|
|
91
|
+
try {
|
|
92
|
+
(0, child_process_1.execSync)('npm install --no-audit --no-fund --no-save --no-package-lock --omit=dev', {
|
|
93
|
+
cwd: baseDir,
|
|
94
|
+
stdio: 'pipe',
|
|
95
|
+
});
|
|
96
|
+
console.log(chalk_1.default.green(`ā
Installed: ${missing.join(', ')}`));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const stderr = error?.stderr?.toString?.() || error?.message || 'unknown error';
|
|
100
|
+
console.log(chalk_1.default.yellow(`ā ļø Could not install user-level deps (${missing.join(', ')}): ${stderr.split('\n')[0]}`));
|
|
101
|
+
console.log(chalk_1.default.gray(` Skills that depend on these will surface a clear error when invoked.`));
|
|
102
|
+
console.log(chalk_1.default.gray(` To retry manually: cd ${baseDir} && npm install`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
47
105
|
/**
|
|
48
106
|
* Set up the user-level FRAIM directory.
|
|
49
107
|
* Creates the personalized-employee structure so FRAIM works outside any project.
|
|
@@ -55,5 +113,6 @@ async function syncUserLevelArtifacts(userFraimDir) {
|
|
|
55
113
|
console.log(chalk_1.default.blue('š¦ Setting up user-level FRAIM directory...'));
|
|
56
114
|
console.log(chalk_1.default.gray(` Target: ${baseDir}`));
|
|
57
115
|
ensureUserLevelDirectories(baseDir);
|
|
116
|
+
ensureUserLevelDependencies(baseDir);
|
|
58
117
|
console.log(chalk_1.default.green('ā
User-level FRAIM directory ready'));
|
|
59
118
|
}
|