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.
@@ -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
+ }
@@ -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
- await scanAndKillOrphanHubs(effective.pid);
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 buildTrayMenu(hubUrl) {
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: 'Open FRAIM Hub',
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: 'About FRAIM Hub',
143
+ label: `About ${name}`,
135
144
  click: () => {
136
145
  void electron_1.dialog.showMessageBox({
137
146
  type: 'info',
138
- title: 'About FRAIM Hub',
139
- message: 'FRAIM Hub',
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: 'Quit FRAIM',
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('FRAIM AI Hub');
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: 'FRAIM AI Hub',
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-runtime.json:', err);
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('FRAIM Hub');
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 RUNTIME_FILE = 'hub-runtime.json';
17
- function hubRuntimeFilePath(fraimDir) {
18
- return path_1.default.join(fraimDir, RUNTIME_FILE);
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 writeHubRuntimeFile(fraimDir, info) {
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 !== '__freeform__' && existing.jobId !== options.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
- return res.json({ version, latest, updateAvailable });
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 employees = this.hostRuntime.detectEmployees();
4074
- const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(body.configuredAgentId, requestedHostId, employees);
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 inferredConversation = requestedConversationId
4079
- ? null
4080
- : this.resolveImplicitResumeConversation({ projectPath, jobId, requestedAgent: configuredAgent, employees });
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
- // Only carry phase state forward when the prior run was interrupted mid-job
4096
- // (status !== 'completed'). A completed conversation is a finished run;
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: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
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
+ }