fraim-hub 2.0.236 → 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 +116 -28
- 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/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.
|
|
@@ -1960,7 +2051,7 @@ class AiHubServer {
|
|
|
1960
2051
|
},
|
|
1961
2052
|
};
|
|
1962
2053
|
}
|
|
1963
|
-
if (existing.jobId && existing.jobId !==
|
|
2054
|
+
if (existing.jobId && existing.jobId !== options.jobId) {
|
|
1964
2055
|
console.warn('[ai-hub] hub.conversation_continuity.rejected', {
|
|
1965
2056
|
conversationId,
|
|
1966
2057
|
existingJobId: existing.jobId,
|
|
@@ -1978,15 +2069,6 @@ class AiHubServer {
|
|
|
1978
2069
|
}
|
|
1979
2070
|
return { ok: true, continuityDecision: 'same_continuity' };
|
|
1980
2071
|
}
|
|
1981
|
-
resolveImplicitResumeConversation(options) {
|
|
1982
|
-
const candidates = this.conversationStore.loadProject(options.projectPath).conversations.filter((conversation) => {
|
|
1983
|
-
if (conversation.jobId !== options.jobId)
|
|
1984
|
-
return false;
|
|
1985
|
-
const existingAgentId = this.configuredAgentIdForConversation(conversation, options.employees);
|
|
1986
|
-
return existingAgentId === options.requestedAgent.id;
|
|
1987
|
-
});
|
|
1988
|
-
return candidates.length === 1 ? candidates[0] : null;
|
|
1989
|
-
}
|
|
1990
2072
|
isTrustedHubOrigin(req) {
|
|
1991
2073
|
const origin = req.get('origin');
|
|
1992
2074
|
if (!origin)
|
|
@@ -3352,7 +3434,15 @@ class AiHubServer {
|
|
|
3352
3434
|
const version = (0, version_utils_1.getFraimVersion)();
|
|
3353
3435
|
const latest = await (0, hub_latest_version_1.getLatestPublishedVersion)();
|
|
3354
3436
|
const updateAvailable = !!(latest && semver.valid(version) && semver.valid(latest) && semver.gt(latest, version));
|
|
3355
|
-
|
|
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
|
+
});
|
|
3356
3446
|
});
|
|
3357
3447
|
// #921: expose the server's pid so the orphan scanner in cli.ts can identify and
|
|
3358
3448
|
// kill Hub processes that are not registered in hub-runtime.json.
|
|
@@ -4070,18 +4160,13 @@ class AiHubServer {
|
|
|
4070
4160
|
throw new Error('A jobId is required to resume.');
|
|
4071
4161
|
if (!instructions)
|
|
4072
4162
|
throw new Error('Provide an instruction to continue.');
|
|
4073
|
-
const
|
|
4074
|
-
const
|
|
4075
|
-
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()
|
|
4076
4165
|
? body.conversationId.trim()
|
|
4077
4166
|
: undefined;
|
|
4078
|
-
const
|
|
4079
|
-
?
|
|
4080
|
-
:
|
|
4081
|
-
const conversationId = requestedConversationId || inferredConversation?.id;
|
|
4082
|
-
const persistedConversation = requestedConversationId
|
|
4083
|
-
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === requestedConversationId)
|
|
4084
|
-
: inferredConversation ?? undefined;
|
|
4167
|
+
const persistedConversation = conversationId
|
|
4168
|
+
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === conversationId)
|
|
4169
|
+
: undefined;
|
|
4085
4170
|
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
4086
4171
|
const now = new Date().toISOString();
|
|
4087
4172
|
const run = {
|
|
@@ -4092,16 +4177,12 @@ class AiHubServer {
|
|
|
4092
4177
|
jobId, hostId, configuredAgentId: configuredAgent.id, configuredAgentLabel: configuredAgent.label, baseHostId: configuredAgent.baseHostId, projectPath, status: 'running', sessionId,
|
|
4093
4178
|
createdAt: now, updatedAt: now, messages: [],
|
|
4094
4179
|
events: [(0, hosts_1.createHubEvent)('system', `Resuming ${configuredAgent.label} (${hostId}) session ${sessionId} in ${projectPath}`)],
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
// resuming the session starts a fresh job, so the tracker must be blank.
|
|
4098
|
-
currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
|
|
4099
|
-
phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
|
|
4180
|
+
currentPhase: persistedRun?.currentPhase || null,
|
|
4181
|
+
phaseHistory: persistedRun?.phaseHistory || [],
|
|
4100
4182
|
totals: persistedRun?.totals || emptyTotals(),
|
|
4101
4183
|
lastStatusChangeAt: now,
|
|
4102
|
-
runDiscriminant:
|
|
4184
|
+
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
4103
4185
|
personaKey: getHubPersonaForJob(jobId),
|
|
4104
|
-
continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
|
|
4105
4186
|
};
|
|
4106
4187
|
// Continue-turn message (FRAIM invocation for the job + instructions) plus
|
|
4107
4188
|
// the shared-browser note so the resumed agent knows about it.
|
|
@@ -5071,6 +5152,13 @@ function resolveAiHubPublicDir() {
|
|
|
5071
5152
|
}
|
|
5072
5153
|
throw new Error('Could not locate public/ai-hub assets.');
|
|
5073
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
|
+
}
|
|
5074
5162
|
// Issue #489: Resolve the word taskpane static assets directory.
|
|
5075
5163
|
// Returns null (not throws) when the directory does not exist so the Hub
|
|
5076
5164
|
// can start without the Word add-in assets present (e.g., older installs).
|
|
@@ -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.defaultHubUiCacheDir = defaultHubUiCacheDir;
|
|
7
|
+
exports.assertSafeHubUiAssetPath = assertSafeHubUiAssetPath;
|
|
8
|
+
exports.hubUiReleaseDir = hubUiReleaseDir;
|
|
9
|
+
exports.readHubUiCacheState = readHubUiCacheState;
|
|
10
|
+
exports.writeHubUiCacheFailure = writeHubUiCacheFailure;
|
|
11
|
+
exports.writeVerifiedHubUiRelease = writeVerifiedHubUiRelease;
|
|
12
|
+
exports.readCachedHubUiManifest = readCachedHubUiManifest;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
16
|
+
function defaultHubUiCacheDir() {
|
|
17
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'hub-ui-cache');
|
|
18
|
+
}
|
|
19
|
+
function assertSafeHubUiAssetPath(assetPath) {
|
|
20
|
+
if (!assetPath ||
|
|
21
|
+
path_1.default.isAbsolute(assetPath) ||
|
|
22
|
+
assetPath.includes('\\') ||
|
|
23
|
+
assetPath.split('/').includes('..')) {
|
|
24
|
+
throw new Error(`unsafe-asset-path:${assetPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function hubUiReleaseDir(cacheDir, releaseId) {
|
|
28
|
+
assertSafeHubUiAssetPath(releaseId);
|
|
29
|
+
return path_1.default.join(cacheDir, 'releases', releaseId);
|
|
30
|
+
}
|
|
31
|
+
function readHubUiCacheState(cacheDir) {
|
|
32
|
+
const statePath = path_1.default.join(cacheDir, 'state.json');
|
|
33
|
+
try {
|
|
34
|
+
if (!fs_1.default.existsSync(statePath))
|
|
35
|
+
return null;
|
|
36
|
+
return JSON.parse(fs_1.default.readFileSync(statePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeHubUiCacheFailure(cacheDir, reason) {
|
|
43
|
+
const existing = readHubUiCacheState(cacheDir);
|
|
44
|
+
fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(cacheDir, 'state.json'), JSON.stringify({
|
|
46
|
+
version: 1,
|
|
47
|
+
activeReleaseId: existing?.activeReleaseId || 'bundled',
|
|
48
|
+
lastKnownGoodReleaseId: existing?.lastKnownGoodReleaseId || 'bundled',
|
|
49
|
+
lastSuccessfulUpdateAt: existing?.lastSuccessfulUpdateAt || '',
|
|
50
|
+
lastFailure: reason,
|
|
51
|
+
}, null, 2));
|
|
52
|
+
}
|
|
53
|
+
async function writeVerifiedHubUiRelease(options) {
|
|
54
|
+
assertSafeHubUiAssetPath(options.manifest.releaseId);
|
|
55
|
+
const releaseDir = hubUiReleaseDir(options.cacheDir, options.manifest.releaseId);
|
|
56
|
+
const tmpDir = `${releaseDir}.tmp-${process.pid}-${Date.now()}`;
|
|
57
|
+
await fs_1.default.promises.rm(tmpDir, { force: true, recursive: true });
|
|
58
|
+
await fs_1.default.promises.mkdir(tmpDir, { recursive: true });
|
|
59
|
+
try {
|
|
60
|
+
for (const asset of options.manifest.assets) {
|
|
61
|
+
assertSafeHubUiAssetPath(asset.path);
|
|
62
|
+
const body = options.assets.get(asset.path);
|
|
63
|
+
if (!body) {
|
|
64
|
+
throw new Error(`missing-verified-asset:${asset.path}`);
|
|
65
|
+
}
|
|
66
|
+
const destination = path_1.default.join(tmpDir, asset.path);
|
|
67
|
+
await fs_1.default.promises.mkdir(path_1.default.dirname(destination), { recursive: true });
|
|
68
|
+
await fs_1.default.promises.writeFile(destination, body);
|
|
69
|
+
}
|
|
70
|
+
await fs_1.default.promises.writeFile(path_1.default.join(tmpDir, 'manifest.json'), JSON.stringify(options.manifest, null, 2));
|
|
71
|
+
await fs_1.default.promises.rm(releaseDir, { force: true, recursive: true });
|
|
72
|
+
await fs_1.default.promises.mkdir(path_1.default.dirname(releaseDir), { recursive: true });
|
|
73
|
+
await fs_1.default.promises.rename(tmpDir, releaseDir);
|
|
74
|
+
await fs_1.default.promises.mkdir(options.cacheDir, { recursive: true });
|
|
75
|
+
await fs_1.default.promises.writeFile(path_1.default.join(options.cacheDir, 'state.json'), JSON.stringify({
|
|
76
|
+
version: 1,
|
|
77
|
+
activeReleaseId: options.manifest.releaseId,
|
|
78
|
+
lastKnownGoodReleaseId: options.manifest.releaseId,
|
|
79
|
+
lastSuccessfulUpdateAt: new Date().toISOString(),
|
|
80
|
+
lastFailure: null,
|
|
81
|
+
}, null, 2));
|
|
82
|
+
return { releaseId: options.manifest.releaseId, releaseDir };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
await fs_1.default.promises.rm(tmpDir, { force: true, recursive: true });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function readCachedHubUiManifest(cacheDir, releaseId) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(hubUiReleaseDir(cacheDir, releaseId), 'manifest.json'), 'utf8'));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|