fraim-hub 2.0.237 → 2.0.238
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-hub-2.js +12 -0
- package/dist/src/ai-hub/cli.js +12 -6
- package/dist/src/ai-hub/desktop-main.js +29 -18
- package/dist/src/ai-hub/hub-runtime-file.js +15 -9
- package/dist/src/ai-hub/hub2-remote-config.js +21 -0
- package/dist/src/ai-hub/server.js +127 -58
- package/dist/src/ai-hub/ui-cache.js +96 -0
- package/dist/src/ai-hub/ui-manifest-client.js +93 -0
- package/dist/src/ai-hub/ui-runtime.js +175 -0
- package/dist/src/cli/fraim-hub-2.js +40 -0
- package/dist/src/cli/fraim-hub.js +11 -6
- package/extensions/office-word/taskpane.html +6 -6
- package/index.js +1 -1
- package/package.json +6 -3
- package/public/ai-hub/index.html +5 -4
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
- package/public/ai-hub/remote-ui-loader.js +63 -0
- package/public/ai-hub/script.js +0 -47
- package/public/ai-hub/styles.css +2 -9
- package/public/portfolio/ashley.html +1 -1
- package/public/portfolio/casey.html +1 -1
- package/public/portfolio/celia.html +1 -1
- package/public/portfolio/hari.html +1 -1
- package/public/portfolio/maestro.html +1 -1
- package/public/portfolio/mandy.html +1 -1
- package/public/portfolio/pam.html +6 -6
- package/public/portfolio/qasm.html +1 -1
- package/public/portfolio/sade.html +1 -1
- package/public/portfolio/swen.html +6 -6
- package/dist/src/ai-hub/word-sideload.js +0 -95
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
const { createFraimHub2Program } = require('../dist/src/cli/fraim-hub-2.js');
|
|
4
|
+
createFraimHub2Program().parseAsync(process.argv).catch((error) => {
|
|
5
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error('Unable to start FRAIM Hub 2. Run npm install -g fraim-hub again to refresh the package.');
|
|
10
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
package/dist/src/ai-hub/cli.js
CHANGED
|
@@ -75,7 +75,7 @@ function resolveDesktopEntry() {
|
|
|
75
75
|
}
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
|
-
function openDesktopWindow(projectPath, preferredPort) {
|
|
78
|
+
function openDesktopWindow(projectPath, preferredPort, runtimeId) {
|
|
79
79
|
const electronBinary = resolveElectronBinary();
|
|
80
80
|
const desktopEntry = resolveDesktopEntry();
|
|
81
81
|
if (!electronBinary || !desktopEntry) {
|
|
@@ -84,6 +84,9 @@ function openDesktopWindow(projectPath, preferredPort) {
|
|
|
84
84
|
const args = projectPath
|
|
85
85
|
? [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)]
|
|
86
86
|
: [desktopEntry, '--port', String(preferredPort)];
|
|
87
|
+
if (runtimeId && runtimeId !== 'hub') {
|
|
88
|
+
args.push('--hub-runtime-id', runtimeId);
|
|
89
|
+
}
|
|
87
90
|
const child = (0, child_process_1.spawn)(electronBinary, args, {
|
|
88
91
|
detached: true,
|
|
89
92
|
stdio: 'ignore',
|
|
@@ -204,8 +207,8 @@ function fetchRunningHubVersion(port) {
|
|
|
204
207
|
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
205
208
|
});
|
|
206
209
|
}
|
|
207
|
-
async function reconcileRunningHub(flags) {
|
|
208
|
-
const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)());
|
|
210
|
+
async function reconcileRunningHub(flags, runtimeId = 'hub') {
|
|
211
|
+
const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), runtimeId);
|
|
209
212
|
const confirmedVersion = running ? await fetchRunningHubVersion(running.port) : null;
|
|
210
213
|
const live = !!(running && confirmedVersion && isProcessAlive(running.pid));
|
|
211
214
|
const effective = live && running ? { ...running, version: confirmedVersion } : null;
|
|
@@ -222,7 +225,9 @@ async function reconcileRunningHub(flags) {
|
|
|
222
225
|
await waitForPortFree(effective.port);
|
|
223
226
|
// #921: after killing the registered pid, scan for orphan Hub processes that were
|
|
224
227
|
// never registered in hub-runtime.json (e.g. older versions started via bare npx).
|
|
225
|
-
|
|
228
|
+
if (runtimeId === 'hub') {
|
|
229
|
+
await scanAndKillOrphanHubs(effective.pid);
|
|
230
|
+
}
|
|
226
231
|
}
|
|
227
232
|
else if (decision.action === 'focus-existing' && effective) {
|
|
228
233
|
console.log(`A Hub (v${effective.version}) is already running - focusing it. Use --restart to replace it.`);
|
|
@@ -232,12 +237,13 @@ async function runHub(options) {
|
|
|
232
237
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
233
238
|
const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
|
|
234
239
|
const projectPath = options.projectPath ? path_1.default.resolve(options.projectPath) : undefined;
|
|
240
|
+
const runtimeId = options.runtimeId || process.env.FRAIM_HUB_RUNTIME_ID || 'hub';
|
|
235
241
|
if (options.open) {
|
|
236
242
|
const wantDesktop = !options.browser;
|
|
237
243
|
if (wantDesktop) {
|
|
238
|
-
await reconcileRunningHub({ restart: !!options.restart, keepRunning: !!options.keepRunning });
|
|
244
|
+
await reconcileRunningHub({ restart: !!options.restart, keepRunning: !!options.keepRunning }, runtimeId);
|
|
239
245
|
}
|
|
240
|
-
const openedDesktop = wantDesktop && openDesktopWindow(projectPath, preferredPort);
|
|
246
|
+
const openedDesktop = wantDesktop && openDesktopWindow(projectPath, preferredPort, runtimeId);
|
|
241
247
|
if (!openedDesktop) {
|
|
242
248
|
const port = await findAvailablePort(preferredPort);
|
|
243
249
|
const server = new AiHubServer(projectPath ? { projectPath } : {});
|
|
@@ -37,6 +37,7 @@ function preferredWindowSize() {
|
|
|
37
37
|
function parseArgs(argv) {
|
|
38
38
|
let projectPath;
|
|
39
39
|
let preferredPort = 43091;
|
|
40
|
+
let runtimeId = process.env.FRAIM_HUB_RUNTIME_ID || 'hub';
|
|
40
41
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
42
|
if (argv[i] === '--project-path' && argv[i + 1]) {
|
|
42
43
|
projectPath = argv[i + 1];
|
|
@@ -46,8 +47,12 @@ function parseArgs(argv) {
|
|
|
46
47
|
preferredPort = Number(argv[i + 1]) || preferredPort;
|
|
47
48
|
i += 1;
|
|
48
49
|
}
|
|
50
|
+
if (argv[i] === '--hub-runtime-id' && argv[i + 1]) {
|
|
51
|
+
runtimeId = argv[i + 1];
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
|
-
return { projectPath, preferredPort };
|
|
55
|
+
return { projectPath, preferredPort, runtimeId };
|
|
51
56
|
}
|
|
52
57
|
function applyUserDataOverride() {
|
|
53
58
|
const userDataDir = process.env.FRAIM_AI_HUB_USER_DATA_DIR;
|
|
@@ -115,10 +120,14 @@ function ensureWordSideload(projectPath, httpPort) {
|
|
|
115
120
|
// ---------------------------------------------------------------------------
|
|
116
121
|
// Tray setup
|
|
117
122
|
// ---------------------------------------------------------------------------
|
|
118
|
-
function
|
|
123
|
+
function displayName(runtimeId = process.env.FRAIM_HUB_RUNTIME_ID || 'hub') {
|
|
124
|
+
return process.env.FRAIM_HUB_DISPLAY_NAME || (runtimeId === 'hub2' ? 'FRAIM Hub 2' : 'FRAIM Hub');
|
|
125
|
+
}
|
|
126
|
+
function buildTrayMenu(hubUrl, runtimeId) {
|
|
127
|
+
const name = displayName(runtimeId);
|
|
119
128
|
return electron_1.Menu.buildFromTemplate([
|
|
120
129
|
{
|
|
121
|
-
label:
|
|
130
|
+
label: `Open ${name}`,
|
|
122
131
|
click: () => {
|
|
123
132
|
if (mainWindow) {
|
|
124
133
|
mainWindow.show();
|
|
@@ -131,12 +140,12 @@ function buildTrayMenu(hubUrl) {
|
|
|
131
140
|
},
|
|
132
141
|
{
|
|
133
142
|
// #755: surface the running build version so staleness is diagnosable at a glance.
|
|
134
|
-
label:
|
|
143
|
+
label: `About ${name}`,
|
|
135
144
|
click: () => {
|
|
136
145
|
void electron_1.dialog.showMessageBox({
|
|
137
146
|
type: 'info',
|
|
138
|
-
title:
|
|
139
|
-
message:
|
|
147
|
+
title: `About ${name}`,
|
|
148
|
+
message: name,
|
|
140
149
|
detail: `Version ${(0, version_utils_1.getFraimVersion)()}\nIdentity: ~/.fraim/config.json\nLocal server: ${hubUrl}`,
|
|
141
150
|
buttons: ['OK'],
|
|
142
151
|
});
|
|
@@ -159,7 +168,7 @@ function buildTrayMenu(hubUrl) {
|
|
|
159
168
|
},
|
|
160
169
|
{ type: 'separator' },
|
|
161
170
|
{
|
|
162
|
-
label:
|
|
171
|
+
label: `Quit ${name}`,
|
|
163
172
|
click: () => {
|
|
164
173
|
isQuitting = true;
|
|
165
174
|
electron_1.app.quit();
|
|
@@ -167,10 +176,11 @@ function buildTrayMenu(hubUrl) {
|
|
|
167
176
|
},
|
|
168
177
|
]);
|
|
169
178
|
}
|
|
170
|
-
function createTray(hubUrl) {
|
|
179
|
+
function createTray(hubUrl, runtimeId) {
|
|
180
|
+
const name = displayName(runtimeId);
|
|
171
181
|
tray = new electron_1.Tray(resolveTrayIcon());
|
|
172
|
-
tray.setToolTip(
|
|
173
|
-
tray.setContextMenu(buildTrayMenu(hubUrl));
|
|
182
|
+
tray.setToolTip(name);
|
|
183
|
+
tray.setContextMenu(buildTrayMenu(hubUrl, runtimeId));
|
|
174
184
|
tray.on('double-click', () => {
|
|
175
185
|
if (mainWindow) {
|
|
176
186
|
mainWindow.show();
|
|
@@ -184,12 +194,12 @@ function createTray(hubUrl) {
|
|
|
184
194
|
// ---------------------------------------------------------------------------
|
|
185
195
|
// BrowserWindow
|
|
186
196
|
// ---------------------------------------------------------------------------
|
|
187
|
-
async function createWindow(url) {
|
|
197
|
+
async function createWindow(url, runtimeId = process.env.FRAIM_HUB_RUNTIME_ID || 'hub') {
|
|
188
198
|
const { width, height } = preferredWindowSize();
|
|
189
199
|
const isMac = process.platform === 'darwin';
|
|
190
200
|
const isWin = process.platform === 'win32';
|
|
191
201
|
mainWindow = new electron_1.BrowserWindow({
|
|
192
|
-
title:
|
|
202
|
+
title: displayName(runtimeId),
|
|
193
203
|
width,
|
|
194
204
|
height,
|
|
195
205
|
minWidth: 1200,
|
|
@@ -309,6 +319,7 @@ function stopServerOnce() {
|
|
|
309
319
|
// Launch
|
|
310
320
|
// ---------------------------------------------------------------------------
|
|
311
321
|
async function launchDesktopShell(options) {
|
|
322
|
+
const runtimeId = options.runtimeId || process.env.FRAIM_HUB_RUNTIME_ID || 'hub';
|
|
312
323
|
const httpPort = await (0, server_1.findAvailablePort)(options.preferredPort);
|
|
313
324
|
const httpsPort = await (0, server_1.findAvailablePortExcluding)(43092, new Set([httpPort]));
|
|
314
325
|
// Generate (or load cached) self-signed cert for HTTPS.
|
|
@@ -339,15 +350,15 @@ async function launchDesktopShell(options) {
|
|
|
339
350
|
port: httpPort,
|
|
340
351
|
version: (0, version_utils_1.getFraimVersion)(),
|
|
341
352
|
startedAt: new Date().toISOString(),
|
|
342
|
-
});
|
|
353
|
+
}, runtimeId);
|
|
343
354
|
}
|
|
344
355
|
catch (err) {
|
|
345
|
-
console.warn('[fraim] could not write hub
|
|
356
|
+
console.warn('[fraim] could not write hub runtime file:', err);
|
|
346
357
|
}
|
|
347
358
|
ensureWordSideload(resolvedProjectPath, httpPort);
|
|
348
359
|
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
|
349
|
-
createTray(hubUrl);
|
|
350
|
-
await createWindow(hubUrl);
|
|
360
|
+
createTray(hubUrl, runtimeId);
|
|
361
|
+
await createWindow(hubUrl, runtimeId);
|
|
351
362
|
}
|
|
352
363
|
// ---------------------------------------------------------------------------
|
|
353
364
|
// Bootstrap
|
|
@@ -372,7 +383,7 @@ async function bootstrap() {
|
|
|
372
383
|
}
|
|
373
384
|
});
|
|
374
385
|
await electron_1.app.whenReady();
|
|
375
|
-
electron_1.app.setName(
|
|
386
|
+
electron_1.app.setName(displayName(options.runtimeId));
|
|
376
387
|
// First-launch housekeeping (idempotent, fast on subsequent runs)
|
|
377
388
|
ensureLoginItem();
|
|
378
389
|
configureAutoUpdater();
|
|
@@ -388,7 +399,7 @@ async function bootstrap() {
|
|
|
388
399
|
// #755: clear the runtime file so a later `fraim hub` doesn't treat a
|
|
389
400
|
// cleanly-exited instance as live.
|
|
390
401
|
try {
|
|
391
|
-
(0, hub_runtime_file_1.removeHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)());
|
|
402
|
+
(0, hub_runtime_file_1.removeHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), options.runtimeId);
|
|
392
403
|
}
|
|
393
404
|
catch { /* best-effort */ }
|
|
394
405
|
void stopServerOnce();
|
|
@@ -13,17 +13,23 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
// `fraim hub` CLI can discover a live instance (pid/port/version) before deciding
|
|
14
14
|
// whether to replace a stale build. `fraimDir` is injected (never hard-coded to
|
|
15
15
|
// the real ~/.fraim) so tests isolate via a temp directory.
|
|
16
|
-
const
|
|
17
|
-
function
|
|
18
|
-
|
|
16
|
+
const DEFAULT_RUNTIME_ID = 'hub';
|
|
17
|
+
function runtimeFileName(runtimeId = DEFAULT_RUNTIME_ID) {
|
|
18
|
+
if (runtimeId === DEFAULT_RUNTIME_ID)
|
|
19
|
+
return 'hub-runtime.json';
|
|
20
|
+
const safeRuntimeId = runtimeId.toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/^-+|-+$/g, '');
|
|
21
|
+
return `${safeRuntimeId || DEFAULT_RUNTIME_ID}-runtime.json`;
|
|
19
22
|
}
|
|
20
|
-
function
|
|
23
|
+
function hubRuntimeFilePath(fraimDir, runtimeId = DEFAULT_RUNTIME_ID) {
|
|
24
|
+
return path_1.default.join(fraimDir, runtimeFileName(runtimeId));
|
|
25
|
+
}
|
|
26
|
+
function writeHubRuntimeFile(fraimDir, info, runtimeId = DEFAULT_RUNTIME_ID) {
|
|
21
27
|
fs_1.default.mkdirSync(fraimDir, { recursive: true });
|
|
22
|
-
fs_1.default.writeFileSync(hubRuntimeFilePath(fraimDir), JSON.stringify(info, null, 2));
|
|
28
|
+
fs_1.default.writeFileSync(hubRuntimeFilePath(fraimDir, runtimeId), JSON.stringify(info, null, 2));
|
|
23
29
|
}
|
|
24
|
-
function readHubRuntimeFile(fraimDir) {
|
|
30
|
+
function readHubRuntimeFile(fraimDir, runtimeId = DEFAULT_RUNTIME_ID) {
|
|
25
31
|
try {
|
|
26
|
-
const parsed = JSON.parse(fs_1.default.readFileSync(hubRuntimeFilePath(fraimDir), 'utf8'));
|
|
32
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(hubRuntimeFilePath(fraimDir, runtimeId), 'utf8'));
|
|
27
33
|
if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number' || typeof parsed.version !== 'string') {
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
@@ -33,9 +39,9 @@ function readHubRuntimeFile(fraimDir) {
|
|
|
33
39
|
return null;
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
|
-
function removeHubRuntimeFile(fraimDir) {
|
|
42
|
+
function removeHubRuntimeFile(fraimDir, runtimeId = DEFAULT_RUNTIME_ID) {
|
|
37
43
|
try {
|
|
38
|
-
fs_1.default.rmSync(hubRuntimeFilePath(fraimDir), { force: true });
|
|
44
|
+
fs_1.default.rmSync(hubRuntimeFilePath(fraimDir, runtimeId), { force: true });
|
|
39
45
|
}
|
|
40
46
|
catch {
|
|
41
47
|
/* no-op: absent file is fine */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HUB2_REMOTE_UI_PUBLIC_KEY_PEM = exports.HUB2_REMOTE_UI_ASSET_ORIGIN = exports.HUB2_REMOTE_UI_RELEASE_ID = exports.HUB2_REMOTE_UI_CHANNEL = void 0;
|
|
4
|
+
exports.hub2RemoteManifestUrl = hub2RemoteManifestUrl;
|
|
5
|
+
exports.hub2TrustedOrigins = hub2TrustedOrigins;
|
|
6
|
+
const remote_hub_gateway_1 = require("./remote-hub-gateway");
|
|
7
|
+
exports.HUB2_REMOTE_UI_CHANNEL = 'hub2';
|
|
8
|
+
exports.HUB2_REMOTE_UI_RELEASE_ID = '2026.07.25.1';
|
|
9
|
+
exports.HUB2_REMOTE_UI_ASSET_ORIGIN = 'https://fraim.wellnessatwork.me';
|
|
10
|
+
exports.HUB2_REMOTE_UI_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
11
|
+
MCowBQYDK2VwAyEAn53Ks3CQSqzflw4/6KIJbKmZeWy94V5H7LYlWhMOePo=
|
|
12
|
+
-----END PUBLIC KEY-----
|
|
13
|
+
`;
|
|
14
|
+
function hub2RemoteManifestUrl(remoteBaseUrl = (0, remote_hub_gateway_1.resolveFraimRemoteUrl)()) {
|
|
15
|
+
const target = new URL('/api/ai-hub/ui/releases/latest', remoteBaseUrl);
|
|
16
|
+
target.searchParams.set('channel', exports.HUB2_REMOTE_UI_CHANNEL);
|
|
17
|
+
return target.toString();
|
|
18
|
+
}
|
|
19
|
+
function hub2TrustedOrigins(remoteBaseUrl = (0, remote_hub_gateway_1.resolveFraimRemoteUrl)()) {
|
|
20
|
+
return Array.from(new Set([new URL(remoteBaseUrl).origin, exports.HUB2_REMOTE_UI_ASSET_ORIGIN])).join(',');
|
|
21
|
+
}
|
|
@@ -66,6 +66,8 @@ const user_config_1 = require("../cli/utils/user-config");
|
|
|
66
66
|
const org_publish_1 = require("../cli/utils/org-publish");
|
|
67
67
|
const version_utils_1 = require("../cli/utils/version-utils");
|
|
68
68
|
const hub_latest_version_1 = require("./hub-latest-version");
|
|
69
|
+
const ui_runtime_1 = require("./ui-runtime");
|
|
70
|
+
const ui_cache_1 = require("./ui-cache");
|
|
69
71
|
const semver = __importStar(require("semver"));
|
|
70
72
|
const BOOTSTRAP_PERSONA_FIRST_PAINT_BUDGET_MS = 250;
|
|
71
73
|
let personaHiringModule;
|
|
@@ -1571,6 +1573,12 @@ class AiHubServer {
|
|
|
1571
1573
|
this.folderPicker = options.folderPicker ?? pickProjectPath;
|
|
1572
1574
|
this.httpsPort = options.httpsPort;
|
|
1573
1575
|
this.certBundle = options.certBundle;
|
|
1576
|
+
this.hubUiCacheDir = options.hubUiCacheDir || process.env.FRAIM_HUB_UI_CACHE_DIR || (0, ui_cache_1.defaultHubUiCacheDir)();
|
|
1577
|
+
this.hubUiPublicKeyPem = options.hubUiPublicKeyPem || process.env.FRAIM_HUB_UI_PUBLIC_KEY;
|
|
1578
|
+
this.hubUiManifestUrl = options.hubUiManifestUrl || process.env.FRAIM_HUB_UI_MANIFEST_URL;
|
|
1579
|
+
this.hubUiTrustedOrigins = options.hubUiTrustedOrigins || parseHubUiTrustedOrigins();
|
|
1580
|
+
this.hubUiRemoteEnabled = options.hubUiRemoteEnabled;
|
|
1581
|
+
this.hubUiAllowInsecureLoopback = options.hubUiAllowInsecureLoopback || process.env.NODE_ENV === 'test';
|
|
1574
1582
|
this.managedBrowser = options.managedBrowser || new managed_browser_1.ManagedBrowser({
|
|
1575
1583
|
channel: process.env.FRAIM_BROWSER_CHANNEL || 'auto',
|
|
1576
1584
|
port: process.env.FRAIM_BROWSER_PORT ? Number(process.env.FRAIM_BROWSER_PORT) : undefined,
|
|
@@ -1626,6 +1634,39 @@ class AiHubServer {
|
|
|
1626
1634
|
next();
|
|
1627
1635
|
});
|
|
1628
1636
|
this.app.use('/ai-hub', express_1.default.static(resolveAiHubPublicDir()));
|
|
1637
|
+
this.app.use('/_fraim-hub-ui', (req, res, next) => {
|
|
1638
|
+
try {
|
|
1639
|
+
const pathname = new URL(req.url, 'http://localhost').pathname;
|
|
1640
|
+
const parts = pathname.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
|
|
1641
|
+
const [releaseId, ...assetParts] = parts;
|
|
1642
|
+
const assetPath = assetParts.join('/');
|
|
1643
|
+
(0, ui_cache_1.assertSafeHubUiAssetPath)(releaseId);
|
|
1644
|
+
(0, ui_cache_1.assertSafeHubUiAssetPath)(assetPath);
|
|
1645
|
+
const releaseDir = (0, ui_cache_1.hubUiReleaseDir)(this.hubUiCacheDir, releaseId);
|
|
1646
|
+
const filePath = path_1.default.join(releaseDir, assetPath);
|
|
1647
|
+
if (!filePath.startsWith(releaseDir + path_1.default.sep) && filePath !== releaseDir) {
|
|
1648
|
+
res.status(403).end();
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const contentTypes = {
|
|
1652
|
+
'.html': 'text/html; charset=utf-8',
|
|
1653
|
+
'.css': 'text/css; charset=utf-8',
|
|
1654
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
1655
|
+
};
|
|
1656
|
+
fs_1.default.readFile(filePath, (err, data) => {
|
|
1657
|
+
if (err) {
|
|
1658
|
+
next();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
1662
|
+
res.setHeader('Content-Type', contentTypes[path_1.default.extname(filePath)] || 'application/octet-stream');
|
|
1663
|
+
res.end(data);
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
catch {
|
|
1667
|
+
res.status(403).end();
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1629
1670
|
// Issue #489: Serve the Word task pane assets at /word-taskpane/*.
|
|
1630
1671
|
// Office JS appends ?_host_Info=Word$Win32$... to every request — we must
|
|
1631
1672
|
// strip the query string before resolving the file path, otherwise every
|
|
@@ -1704,6 +1745,17 @@ class AiHubServer {
|
|
|
1704
1745
|
next();
|
|
1705
1746
|
});
|
|
1706
1747
|
}
|
|
1748
|
+
this.app.get('/api/ai-hub/bridge', (_req, res) => {
|
|
1749
|
+
res.json(this.hubUiBridgeInfo());
|
|
1750
|
+
});
|
|
1751
|
+
this.app.get('/api/ai-hub/ui-runtime', async (_req, res) => {
|
|
1752
|
+
try {
|
|
1753
|
+
res.json(await this.resolveHubUiRuntimeResponse());
|
|
1754
|
+
}
|
|
1755
|
+
catch (error) {
|
|
1756
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1707
1759
|
this.registerRoutes();
|
|
1708
1760
|
}
|
|
1709
1761
|
getApp() {
|
|
@@ -1793,6 +1845,45 @@ class AiHubServer {
|
|
|
1793
1845
|
this.managedBrowser.stop();
|
|
1794
1846
|
}
|
|
1795
1847
|
getHttpsPort() { return this.httpsPort; }
|
|
1848
|
+
isHubUiRemoteEnabled() {
|
|
1849
|
+
if (this.hubUiRemoteEnabled !== undefined)
|
|
1850
|
+
return this.hubUiRemoteEnabled;
|
|
1851
|
+
return process.env.FRAIM_HUB_REMOTE_UI === '1';
|
|
1852
|
+
}
|
|
1853
|
+
hubUiBridgeInfo() {
|
|
1854
|
+
return (0, ui_runtime_1.createAiHubBridgeInfo)({
|
|
1855
|
+
bridgeVersion: (0, version_utils_1.getFraimVersion)(),
|
|
1856
|
+
remoteBaseUrl: (0, remote_hub_gateway_1.resolveFraimRemoteUrl)(),
|
|
1857
|
+
remoteUiEnabled: this.isHubUiRemoteEnabled(),
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
resolveHubUiManifestUrl() {
|
|
1861
|
+
if (this.hubUiManifestUrl)
|
|
1862
|
+
return this.hubUiManifestUrl;
|
|
1863
|
+
if (!this.isHubUiRemoteEnabled())
|
|
1864
|
+
return undefined;
|
|
1865
|
+
return `${(0, remote_hub_gateway_1.resolveFraimRemoteUrl)()}/api/ai-hub/ui/releases/latest`;
|
|
1866
|
+
}
|
|
1867
|
+
resolveHubUiTrustedOrigins(manifestUrl) {
|
|
1868
|
+
if (this.hubUiTrustedOrigins?.length)
|
|
1869
|
+
return this.hubUiTrustedOrigins;
|
|
1870
|
+
if (!manifestUrl)
|
|
1871
|
+
return [];
|
|
1872
|
+
return [new URL(manifestUrl).origin];
|
|
1873
|
+
}
|
|
1874
|
+
resolveHubUiRuntimeResponse() {
|
|
1875
|
+
const bridge = this.hubUiBridgeInfo();
|
|
1876
|
+
const manifestUrl = this.resolveHubUiManifestUrl();
|
|
1877
|
+
return (0, ui_runtime_1.resolveHubUiRuntime)({
|
|
1878
|
+
bridge,
|
|
1879
|
+
cacheDir: this.hubUiCacheDir,
|
|
1880
|
+
remoteEnabled: this.isHubUiRemoteEnabled(),
|
|
1881
|
+
manifestUrl,
|
|
1882
|
+
trustedOrigins: this.resolveHubUiTrustedOrigins(manifestUrl),
|
|
1883
|
+
publicKeyPem: this.hubUiPublicKeyPem,
|
|
1884
|
+
allowInsecureLoopback: this.hubUiAllowInsecureLoopback,
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1796
1887
|
knownProjects(projectPath, extras = [], options = {}) {
|
|
1797
1888
|
// #866 R2: guard against path.resolve('') === cwd. When there is no active
|
|
1798
1889
|
// project, keep the path empty so no invocation-directory project is injected.
|
|
@@ -1848,18 +1939,12 @@ class AiHubServer {
|
|
|
1848
1939
|
console.warn('[ai-hub] manager-team lookup failed:', err?.message || err);
|
|
1849
1940
|
return [];
|
|
1850
1941
|
});
|
|
1851
|
-
const personaProjectionPromise = this.computePersonas(resolvedApiKey, managerTeamPromise
|
|
1852
|
-
const fallbackPersonaProjection = this.fallbackPersonaProjection(resolvedApiKey
|
|
1942
|
+
const personaProjectionPromise = this.computePersonas(resolvedApiKey, managerTeamPromise);
|
|
1943
|
+
const fallbackPersonaProjection = this.fallbackPersonaProjection(resolvedApiKey);
|
|
1853
1944
|
const personaProjection = await this.withFirstPaintBudget(personaProjectionPromise, fallbackPersonaProjection, 'persona projection');
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
// when the authority definitively locks everything (401 / feature-off). So a real
|
|
1858
|
-
// sign-out is still `resolved`, and only a budget miss is `unresolved`.
|
|
1859
|
-
const personasResolved = personaProjection !== fallbackPersonaProjection;
|
|
1860
|
-
const managerTeam = personasResolved
|
|
1861
|
-
? await this.withFirstPaintBudget(managerTeamPromise, [], 'manager team')
|
|
1862
|
-
: [];
|
|
1945
|
+
const managerTeam = personaProjection === fallbackPersonaProjection
|
|
1946
|
+
? []
|
|
1947
|
+
: await this.withFirstPaintBudget(managerTeamPromise, [], 'manager team');
|
|
1863
1948
|
const { personas, subscriptionActive, workspaceId, userKey } = personaProjection;
|
|
1864
1949
|
void personaProjectionPromise.catch(() => undefined);
|
|
1865
1950
|
void managerTeamPromise.catch(() => undefined);
|
|
@@ -1894,9 +1979,6 @@ class AiHubServer {
|
|
|
1894
1979
|
employees,
|
|
1895
1980
|
configuredAgents,
|
|
1896
1981
|
personas,
|
|
1897
|
-
// Issue #1005: lets the client tell a resolved projection from the first-paint
|
|
1898
|
-
// placeholder, so a late unresolved bootstrap cannot wipe a resolved roster.
|
|
1899
|
-
personasResolved,
|
|
1900
1982
|
subscriptionActive,
|
|
1901
1983
|
activeRun,
|
|
1902
1984
|
projects,
|
|
@@ -1969,7 +2051,7 @@ class AiHubServer {
|
|
|
1969
2051
|
},
|
|
1970
2052
|
};
|
|
1971
2053
|
}
|
|
1972
|
-
if (existing.jobId && existing.jobId !==
|
|
2054
|
+
if (existing.jobId && existing.jobId !== options.jobId) {
|
|
1973
2055
|
console.warn('[ai-hub] hub.conversation_continuity.rejected', {
|
|
1974
2056
|
conversationId,
|
|
1975
2057
|
existingJobId: existing.jobId,
|
|
@@ -1987,15 +2069,6 @@ class AiHubServer {
|
|
|
1987
2069
|
}
|
|
1988
2070
|
return { ok: true, continuityDecision: 'same_continuity' };
|
|
1989
2071
|
}
|
|
1990
|
-
resolveImplicitResumeConversation(options) {
|
|
1991
|
-
const candidates = this.conversationStore.loadProject(options.projectPath).conversations.filter((conversation) => {
|
|
1992
|
-
if (conversation.jobId !== options.jobId)
|
|
1993
|
-
return false;
|
|
1994
|
-
const existingAgentId = this.configuredAgentIdForConversation(conversation, options.employees);
|
|
1995
|
-
return existingAgentId === options.requestedAgent.id;
|
|
1996
|
-
});
|
|
1997
|
-
return candidates.length === 1 ? candidates[0] : null;
|
|
1998
|
-
}
|
|
1999
2072
|
isTrustedHubOrigin(req) {
|
|
2000
2073
|
const origin = req.get('origin');
|
|
2001
2074
|
if (!origin)
|
|
@@ -2606,15 +2679,9 @@ class AiHubServer {
|
|
|
2606
2679
|
const message = (invocationForm ?? (0, manager_turns_1.buildSameJobContinueMessage)(userText)) + (0, manager_turns_1.buildCommunicationStyleNote)();
|
|
2607
2680
|
return { message, display };
|
|
2608
2681
|
}
|
|
2609
|
-
async computePersonas(apiKey, managerTeamPromise
|
|
2610
|
-
// Issue #1005: the project whose custom employees belong in this projection. Custom
|
|
2611
|
-
// employees are per-project (`fraim/personalized-employee/employees/`), so reading
|
|
2612
|
-
// them from `this.projectPath` (the directory the Hub launched from) leaked one
|
|
2613
|
-
// project's employees into every other project's roster. Defaults to the launch
|
|
2614
|
-
// project only so callers that genuinely have no project context keep working.
|
|
2615
|
-
projectPath = this.projectPath) {
|
|
2682
|
+
async computePersonas(apiKey, managerTeamPromise) {
|
|
2616
2683
|
const allBundles = listHubPersonaBundles();
|
|
2617
|
-
const fallbackProjection = this.fallbackPersonaProjection(apiKey
|
|
2684
|
+
const fallbackProjection = this.fallbackPersonaProjection(apiKey);
|
|
2618
2685
|
// Issue #701: persona state comes from the hosted server (GET /api/personas/me)
|
|
2619
2686
|
// via the user's API key — never a local MongoDB connection. Issue #925: resolve
|
|
2620
2687
|
// it in a transport-aware way so a transient outage does not collapse to the same
|
|
@@ -2631,7 +2698,7 @@ class AiHubServer {
|
|
|
2631
2698
|
// (feature-off, legacy bypass, per-entitlement gating) lives solely in
|
|
2632
2699
|
// persona-entitlement-service.resolvePersonaAccessStatuses — the Hub does not
|
|
2633
2700
|
// re-derive it.
|
|
2634
|
-
const customPersonas = (0, custom_employees_1.readCustomEmployees)(projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2701
|
+
const customPersonas = (0, custom_employees_1.readCustomEmployees)(this.projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2635
2702
|
if (!state) {
|
|
2636
2703
|
return { ...fallbackProjection, userKey };
|
|
2637
2704
|
}
|
|
@@ -2670,9 +2737,7 @@ class AiHubServer {
|
|
|
2670
2737
|
return { ...fallbackProjection, userKey };
|
|
2671
2738
|
}
|
|
2672
2739
|
}
|
|
2673
|
-
fallbackPersonaProjection(apiKey
|
|
2674
|
-
// Issue #1005: see computePersonas — custom employees are per-project.
|
|
2675
|
-
projectPath = this.projectPath) {
|
|
2740
|
+
fallbackPersonaProjection(apiKey) {
|
|
2676
2741
|
const fallbackPersonas = listHubPersonaBundles().map((bundle) => ({
|
|
2677
2742
|
key: bundle.personaKey,
|
|
2678
2743
|
displayName: bundle.catalogMetadata.displayName,
|
|
@@ -2685,7 +2750,7 @@ class AiHubServer {
|
|
|
2685
2750
|
seatsInUse: 0,
|
|
2686
2751
|
origin: 'catalog',
|
|
2687
2752
|
}));
|
|
2688
|
-
const customPersonas = (0, custom_employees_1.readCustomEmployees)(projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2753
|
+
const customPersonas = (0, custom_employees_1.readCustomEmployees)(this.projectPath).map(custom_employees_1.buildCustomEmployeePersona);
|
|
2689
2754
|
return {
|
|
2690
2755
|
personas: [...fallbackPersonas, ...customPersonas],
|
|
2691
2756
|
subscriptionActive: false,
|
|
@@ -2853,9 +2918,7 @@ class AiHubServer {
|
|
|
2853
2918
|
console.warn('[ai-hub] manager-team lookup failed:', err?.message || err);
|
|
2854
2919
|
return [];
|
|
2855
2920
|
});
|
|
2856
|
-
|
|
2857
|
-
// not to the directory the Hub launched from.
|
|
2858
|
-
const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(apiKey, managerTeamPromise, projectPath);
|
|
2921
|
+
const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(apiKey, managerTeamPromise);
|
|
2859
2922
|
const managerTeam = await managerTeamPromise;
|
|
2860
2923
|
const jobCount = (0, catalog_1.discoverEmployeeJobs)(projectPath, { includeRegistry: true })
|
|
2861
2924
|
.filter((job) => !FRAIM_INTERNAL_JOB_IDS.has(job.id))
|
|
@@ -3371,7 +3434,15 @@ class AiHubServer {
|
|
|
3371
3434
|
const version = (0, version_utils_1.getFraimVersion)();
|
|
3372
3435
|
const latest = await (0, hub_latest_version_1.getLatestPublishedVersion)();
|
|
3373
3436
|
const updateAvailable = !!(latest && semver.valid(version) && semver.valid(latest) && semver.gt(latest, version));
|
|
3374
|
-
|
|
3437
|
+
const uiRuntime = await this.resolveHubUiRuntimeResponse();
|
|
3438
|
+
return res.json({
|
|
3439
|
+
version,
|
|
3440
|
+
latest,
|
|
3441
|
+
updateAvailable,
|
|
3442
|
+
uiReleaseId: uiRuntime.releaseId,
|
|
3443
|
+
uiMode: uiRuntime.mode,
|
|
3444
|
+
uiUpdateAvailable: uiRuntime.mode === 'remote-cache',
|
|
3445
|
+
});
|
|
3375
3446
|
});
|
|
3376
3447
|
// #921: expose the server's pid so the orphan scanner in cli.ts can identify and
|
|
3377
3448
|
// kill Hub processes that are not registered in hub-runtime.json.
|
|
@@ -4089,18 +4160,13 @@ class AiHubServer {
|
|
|
4089
4160
|
throw new Error('A jobId is required to resume.');
|
|
4090
4161
|
if (!instructions)
|
|
4091
4162
|
throw new Error('Provide an instruction to continue.');
|
|
4092
|
-
const
|
|
4093
|
-
const
|
|
4094
|
-
const requestedConversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
4163
|
+
const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(body.configuredAgentId, requestedHostId);
|
|
4164
|
+
const conversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
4095
4165
|
? body.conversationId.trim()
|
|
4096
4166
|
: undefined;
|
|
4097
|
-
const
|
|
4098
|
-
?
|
|
4099
|
-
:
|
|
4100
|
-
const conversationId = requestedConversationId || inferredConversation?.id;
|
|
4101
|
-
const persistedConversation = requestedConversationId
|
|
4102
|
-
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === requestedConversationId)
|
|
4103
|
-
: inferredConversation ?? undefined;
|
|
4167
|
+
const persistedConversation = conversationId
|
|
4168
|
+
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === conversationId)
|
|
4169
|
+
: undefined;
|
|
4104
4170
|
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
4105
4171
|
const now = new Date().toISOString();
|
|
4106
4172
|
const run = {
|
|
@@ -4111,16 +4177,12 @@ class AiHubServer {
|
|
|
4111
4177
|
jobId, hostId, configuredAgentId: configuredAgent.id, configuredAgentLabel: configuredAgent.label, baseHostId: configuredAgent.baseHostId, projectPath, status: 'running', sessionId,
|
|
4112
4178
|
createdAt: now, updatedAt: now, messages: [],
|
|
4113
4179
|
events: [(0, hosts_1.createHubEvent)('system', `Resuming ${configuredAgent.label} (${hostId}) session ${sessionId} in ${projectPath}`)],
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
// resuming the session starts a fresh job, so the tracker must be blank.
|
|
4117
|
-
currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
|
|
4118
|
-
phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
|
|
4180
|
+
currentPhase: persistedRun?.currentPhase || null,
|
|
4181
|
+
phaseHistory: persistedRun?.phaseHistory || [],
|
|
4119
4182
|
totals: persistedRun?.totals || emptyTotals(),
|
|
4120
4183
|
lastStatusChangeAt: now,
|
|
4121
|
-
runDiscriminant:
|
|
4184
|
+
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
4122
4185
|
personaKey: getHubPersonaForJob(jobId),
|
|
4123
|
-
continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
|
|
4124
4186
|
};
|
|
4125
4187
|
// Continue-turn message (FRAIM invocation for the job + instructions) plus
|
|
4126
4188
|
// the shared-browser note so the resumed agent knows about it.
|
|
@@ -5090,6 +5152,13 @@ function resolveAiHubPublicDir() {
|
|
|
5090
5152
|
}
|
|
5091
5153
|
throw new Error('Could not locate public/ai-hub assets.');
|
|
5092
5154
|
}
|
|
5155
|
+
function parseHubUiTrustedOrigins() {
|
|
5156
|
+
const raw = process.env.FRAIM_HUB_UI_TRUSTED_ORIGINS;
|
|
5157
|
+
if (!raw)
|
|
5158
|
+
return undefined;
|
|
5159
|
+
const origins = raw.split(',').map((part) => part.trim()).filter(Boolean);
|
|
5160
|
+
return origins.length ? origins : undefined;
|
|
5161
|
+
}
|
|
5093
5162
|
// Issue #489: Resolve the word taskpane static assets directory.
|
|
5094
5163
|
// Returns null (not throws) when the directory does not exist so the Hub
|
|
5095
5164
|
// can start without the Word add-in assets present (e.g., older installs).
|