fraim-hub 2.0.227 → 2.0.229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ai-hub/conversation-store.js +102 -8
- package/dist/src/ai-hub/desktop-main.js +10 -0
- package/dist/src/ai-hub/hosts.js +10 -0
- package/dist/src/ai-hub/hub-install.js +3 -6
- package/dist/src/ai-hub/hub-release-download.js +123 -0
- package/dist/src/ai-hub/remote-hub-gateway.js +14 -7
- package/dist/src/ai-hub/server.js +107 -30
- package/package.json +2 -2
- package/public/ai-hub/index.html +10 -3
- package/public/ai-hub/script.js +309 -184
- package/public/ai-hub/styles.css +181 -136
|
@@ -19,7 +19,18 @@ exports.COMPANY_SCOPE_KEY = '@company';
|
|
|
19
19
|
// (id, title, jobId, jobTitle, agentName, personaKey, status, runId, createdAt, lastUpdatedAt,
|
|
20
20
|
// scope, reviewHandoff, ...) is kept. Client-only runtime flags are omitted too so stale index
|
|
21
21
|
// cache state cannot make the frontend skip lazy body hydration.
|
|
22
|
-
const HEADER_OMITTED_FIELDS = [
|
|
22
|
+
const HEADER_OMITTED_FIELDS = [
|
|
23
|
+
'messages',
|
|
24
|
+
'events',
|
|
25
|
+
'artifacts',
|
|
26
|
+
'run',
|
|
27
|
+
'delegation',
|
|
28
|
+
'_bodyLoaded',
|
|
29
|
+
'_stopping',
|
|
30
|
+
'_priorEvents',
|
|
31
|
+
'_priorMessages',
|
|
32
|
+
];
|
|
33
|
+
const HEADER_OMITTED_FIELD_SET = new Set(HEADER_OMITTED_FIELDS);
|
|
23
34
|
/**
|
|
24
35
|
* Issue #708: resolve the conversation store bucket key for a given scope.
|
|
25
36
|
* - 'manager'/'company' → a stable sentinel key (project-independent home).
|
|
@@ -292,8 +303,36 @@ function toHeader(conv) {
|
|
|
292
303
|
const header = { ...conv };
|
|
293
304
|
for (const field of HEADER_OMITTED_FIELDS)
|
|
294
305
|
delete header[field];
|
|
306
|
+
for (const field of Object.keys(header)) {
|
|
307
|
+
if (field.startsWith('_'))
|
|
308
|
+
delete header[field];
|
|
309
|
+
}
|
|
295
310
|
return header;
|
|
296
311
|
}
|
|
312
|
+
function headerNeedsSanitization(header) {
|
|
313
|
+
const value = header;
|
|
314
|
+
return Object.keys(value).some((field) => HEADER_OMITTED_FIELD_SET.has(field) || field.startsWith('_'));
|
|
315
|
+
}
|
|
316
|
+
function sanitizeBucketIndex(index) {
|
|
317
|
+
let changed = false;
|
|
318
|
+
const headers = index.headers.map((header) => {
|
|
319
|
+
if (headerNeedsSanitization(header))
|
|
320
|
+
changed = true;
|
|
321
|
+
return toHeader(header);
|
|
322
|
+
}).sort(newestFirst);
|
|
323
|
+
const activeId = index.activeId && headers.some((header) => header.id === index.activeId) ? index.activeId : null;
|
|
324
|
+
if (activeId !== index.activeId)
|
|
325
|
+
changed = true;
|
|
326
|
+
if (!changed) {
|
|
327
|
+
for (let indexPosition = 0; indexPosition < headers.length; indexPosition += 1) {
|
|
328
|
+
if (headers[indexPosition].id !== index.headers[indexPosition]?.id) {
|
|
329
|
+
changed = true;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { index: { activeId, headers }, changed };
|
|
335
|
+
}
|
|
297
336
|
function renameWithRetry(from, to) {
|
|
298
337
|
// Windows can throw EPERM/EBUSY when a reader briefly holds the destination open. Retry a few
|
|
299
338
|
// times; the per-bucket lock already prevents concurrent writers from colliding here.
|
|
@@ -322,6 +361,7 @@ class AiHubConversationStore {
|
|
|
322
361
|
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-conversations.json')) {
|
|
323
362
|
this.stateFilePath = stateFilePath;
|
|
324
363
|
this.migrated = false;
|
|
364
|
+
this.projectPathCache = null;
|
|
325
365
|
const dir = path_1.default.dirname(this.stateFilePath);
|
|
326
366
|
const base = path_1.default.basename(this.stateFilePath).replace(/\.json$/i, '');
|
|
327
367
|
// Sibling directory of the legacy file, e.g. ~/.fraim/ai-hub-conversations/
|
|
@@ -345,6 +385,9 @@ class AiHubConversationStore {
|
|
|
345
385
|
lockPath(bucketDir) {
|
|
346
386
|
return path_1.default.join(bucketDir, '.lock');
|
|
347
387
|
}
|
|
388
|
+
invalidateProjectPathCache() {
|
|
389
|
+
this.projectPathCache = null;
|
|
390
|
+
}
|
|
348
391
|
// ---- migration (split of the legacy monolith) ----
|
|
349
392
|
importLegacyMonolithIfPresent() {
|
|
350
393
|
let stat;
|
|
@@ -431,8 +474,23 @@ class AiHubConversationStore {
|
|
|
431
474
|
// source of truth) and persist it best-effort.
|
|
432
475
|
loadIndex(bucketDir, bucketKey) {
|
|
433
476
|
const existing = this.readIndex(bucketDir);
|
|
434
|
-
if (existing)
|
|
435
|
-
|
|
477
|
+
if (existing) {
|
|
478
|
+
const sanitized = sanitizeBucketIndex(existing);
|
|
479
|
+
if (sanitized.changed) {
|
|
480
|
+
try {
|
|
481
|
+
writeJsonAtomic(this.indexPath(bucketDir), {
|
|
482
|
+
version: 2,
|
|
483
|
+
bucketKey,
|
|
484
|
+
activeId: sanitized.index.activeId,
|
|
485
|
+
headers: sanitized.index.headers,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
/* best-effort cache self-heal */
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return sanitized.index;
|
|
493
|
+
}
|
|
436
494
|
const headers = this.readAllConversations(bucketDir, bucketKey).map(toHeader).sort(newestFirst);
|
|
437
495
|
const rebuilt = { activeId: null, headers };
|
|
438
496
|
if (headers.length > 0) {
|
|
@@ -457,6 +515,7 @@ class AiHubConversationStore {
|
|
|
457
515
|
// Author a whole bucket to exactly `conversations` (used by migration and replaceProject).
|
|
458
516
|
writeBucket(bucketKey, state) {
|
|
459
517
|
const bucketDir = this.bucketDir(bucketKey);
|
|
518
|
+
const bucketAlreadyExisted = fs_1.default.existsSync(bucketDir);
|
|
460
519
|
fs_1.default.mkdirSync(bucketDir, { recursive: true });
|
|
461
520
|
(0, conversation_store_lock_1.withBucketLock)(this.lockPath(bucketDir), () => {
|
|
462
521
|
const desired = new Set(state.conversations.map((c) => c.id));
|
|
@@ -480,10 +539,24 @@ class AiHubConversationStore {
|
|
|
480
539
|
this.writeConvFile(bucketDir, bucketKey, conv);
|
|
481
540
|
this.writeIndex(bucketDir, bucketKey, state.activeId, state.conversations);
|
|
482
541
|
});
|
|
542
|
+
if (!bucketAlreadyExisted)
|
|
543
|
+
this.invalidateProjectPathCache();
|
|
483
544
|
}
|
|
484
545
|
// ---- public API ----
|
|
485
546
|
listProjectPaths() {
|
|
486
547
|
this.ensureMigrated();
|
|
548
|
+
let rootStat;
|
|
549
|
+
try {
|
|
550
|
+
rootStat = fs_1.default.statSync(this.shardRoot);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
if (!rootStat.isDirectory())
|
|
556
|
+
return [];
|
|
557
|
+
if (this.projectPathCache && this.projectPathCache.shardRootMtimeMs === rootStat.mtimeMs) {
|
|
558
|
+
return this.projectPathCache.keys.slice();
|
|
559
|
+
}
|
|
487
560
|
let dirs;
|
|
488
561
|
try {
|
|
489
562
|
dirs = fs_1.default.readdirSync(this.shardRoot);
|
|
@@ -497,7 +570,15 @@ class AiHubConversationStore {
|
|
|
497
570
|
if (bucketKey)
|
|
498
571
|
keys.push(normalizeProjectPath(bucketKey));
|
|
499
572
|
}
|
|
500
|
-
|
|
573
|
+
let finalRootMtimeMs = rootStat.mtimeMs;
|
|
574
|
+
try {
|
|
575
|
+
finalRootMtimeMs = fs_1.default.statSync(this.shardRoot).mtimeMs;
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
/* keep the pre-scan mtime */
|
|
579
|
+
}
|
|
580
|
+
this.projectPathCache = { shardRootMtimeMs: finalRootMtimeMs, keys };
|
|
581
|
+
return keys.slice();
|
|
501
582
|
}
|
|
502
583
|
bucketKeyOf(bucketDir) {
|
|
503
584
|
try {
|
|
@@ -575,6 +656,7 @@ class AiHubConversationStore {
|
|
|
575
656
|
fs_1.default.rmSync(path_1.default.join(bucketDir, file), { recursive: true, force: true });
|
|
576
657
|
}
|
|
577
658
|
});
|
|
659
|
+
this.invalidateProjectPathCache();
|
|
578
660
|
return true;
|
|
579
661
|
}
|
|
580
662
|
replaceProject(projectPath, next) {
|
|
@@ -591,8 +673,9 @@ class AiHubConversationStore {
|
|
|
591
673
|
if (!incoming)
|
|
592
674
|
return this.loadProject(key);
|
|
593
675
|
const bucketDir = this.bucketDir(key);
|
|
676
|
+
const bucketAlreadyExisted = fs_1.default.existsSync(bucketDir);
|
|
594
677
|
fs_1.default.mkdirSync(bucketDir, { recursive: true });
|
|
595
|
-
|
|
678
|
+
const state = (0, conversation_store_lock_1.withBucketLock)(this.lockPath(bucketDir), () => {
|
|
596
679
|
// Merge with an existing copy of the same id, keeping the richer copy (a header-only shell
|
|
597
680
|
// must never overwrite real run history), tie-broken by newer lastUpdatedAt.
|
|
598
681
|
const convPath = this.convFilePath(bucketDir, incoming.id);
|
|
@@ -610,6 +693,9 @@ class AiHubConversationStore {
|
|
|
610
693
|
this.writeConvFile(bucketDir, key, winner);
|
|
611
694
|
return this.reindexAfterUpsert(bucketDir, key, winner, activeId);
|
|
612
695
|
});
|
|
696
|
+
if (!bucketAlreadyExisted)
|
|
697
|
+
this.invalidateProjectPathCache();
|
|
698
|
+
return state;
|
|
613
699
|
}
|
|
614
700
|
// Set the active conversation without rewriting any conversation body. (Mutation methods return
|
|
615
701
|
// header-shaped state, so callers must never round-trip that through replaceProject to set
|
|
@@ -618,20 +704,25 @@ class AiHubConversationStore {
|
|
|
618
704
|
this.ensureMigrated();
|
|
619
705
|
const key = normalizeConversationKey(projectPath);
|
|
620
706
|
const bucketDir = this.bucketDir(key);
|
|
707
|
+
const bucketAlreadyExisted = fs_1.default.existsSync(bucketDir);
|
|
621
708
|
fs_1.default.mkdirSync(bucketDir, { recursive: true });
|
|
622
|
-
|
|
709
|
+
const state = (0, conversation_store_lock_1.withBucketLock)(this.lockPath(bucketDir), () => {
|
|
623
710
|
const idx = this.loadIndex(bucketDir, key);
|
|
624
711
|
const valid = activeId && idx.headers.some((h) => h.id === activeId) ? activeId : null;
|
|
625
712
|
writeJsonAtomic(this.indexPath(bucketDir), { version: 2, bucketKey: key, activeId: valid, headers: idx.headers });
|
|
626
713
|
return normalizeProjectState(key, { activeId: valid, conversations: idx.headers });
|
|
627
714
|
});
|
|
715
|
+
if (!bucketAlreadyExisted)
|
|
716
|
+
this.invalidateProjectPathCache();
|
|
717
|
+
return state;
|
|
628
718
|
}
|
|
629
719
|
patchConversation(projectPath, conversationId, patch) {
|
|
630
720
|
this.ensureMigrated();
|
|
631
721
|
const key = normalizeConversationKey(projectPath);
|
|
632
722
|
const bucketDir = this.bucketDir(key);
|
|
723
|
+
const bucketAlreadyExisted = fs_1.default.existsSync(bucketDir);
|
|
633
724
|
fs_1.default.mkdirSync(bucketDir, { recursive: true });
|
|
634
|
-
|
|
725
|
+
const state = (0, conversation_store_lock_1.withBucketLock)(this.lockPath(bucketDir), () => {
|
|
635
726
|
const convPath = this.convFilePath(bucketDir, conversationId);
|
|
636
727
|
let existing = null;
|
|
637
728
|
try {
|
|
@@ -660,10 +751,13 @@ class AiHubConversationStore {
|
|
|
660
751
|
this.writeConvFile(bucketDir, key, merged);
|
|
661
752
|
return this.reindexAfterUpsert(bucketDir, key, merged, undefined);
|
|
662
753
|
});
|
|
754
|
+
if (!bucketAlreadyExisted)
|
|
755
|
+
this.invalidateProjectPathCache();
|
|
756
|
+
return state;
|
|
663
757
|
}
|
|
664
758
|
// Update the index header for one conversation and return the (header-shaped) project state.
|
|
665
759
|
reindexAfterUpsert(bucketDir, bucketKey, conv, activeId) {
|
|
666
|
-
const idx = this.
|
|
760
|
+
const idx = this.loadIndex(bucketDir, bucketKey);
|
|
667
761
|
const headers = idx.headers.filter((h) => h.id !== conv.id);
|
|
668
762
|
headers.push(toHeader(conv));
|
|
669
763
|
headers.sort(newestFirst);
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.launchDesktopShell = launchDesktopShell;
|
|
7
7
|
const electron_1 = require("electron");
|
|
8
|
+
const electron_updater_1 = require("electron-updater");
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
10
11
|
const server_1 = require("./server");
|
|
@@ -82,6 +83,14 @@ function ensureLoginItem() {
|
|
|
82
83
|
fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
|
|
83
84
|
fs_1.default.writeFileSync(flagPath, '1');
|
|
84
85
|
}
|
|
86
|
+
function configureAutoUpdater() {
|
|
87
|
+
if (!electron_1.app.isPackaged)
|
|
88
|
+
return;
|
|
89
|
+
electron_updater_1.autoUpdater.autoDownload = true;
|
|
90
|
+
electron_updater_1.autoUpdater.checkForUpdatesAndNotify().catch((err) => {
|
|
91
|
+
console.warn('[fraim] auto-update check failed:', err);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
85
94
|
// ---------------------------------------------------------------------------
|
|
86
95
|
// Word manifest sideload (runs once on first launch)
|
|
87
96
|
// ---------------------------------------------------------------------------
|
|
@@ -366,6 +375,7 @@ async function bootstrap() {
|
|
|
366
375
|
electron_1.app.setName('FRAIM Hub');
|
|
367
376
|
// First-launch housekeeping (idempotent, fast on subsequent runs)
|
|
368
377
|
ensureLoginItem();
|
|
378
|
+
configureAutoUpdater();
|
|
369
379
|
electron_1.app.on('activate', () => {
|
|
370
380
|
// macOS: clicking dock icon re-shows the window
|
|
371
381
|
if (mainWindow) {
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.detectEmployees = detectEmployees;
|
|
|
12
12
|
exports.prepareCodexBrowserHome = prepareCodexBrowserHome;
|
|
13
13
|
exports.sharedBrowserHostConfig = sharedBrowserHostConfig;
|
|
14
14
|
exports.buildStartPlan = buildStartPlan;
|
|
15
|
+
exports.buildInteractiveResumeCommand = buildInteractiveResumeCommand;
|
|
15
16
|
exports.buildContinuePlan = buildContinuePlan;
|
|
16
17
|
exports.supportsDirectPath = supportsDirectPath;
|
|
17
18
|
exports.buildDirectStartPlan = buildDirectStartPlan;
|
|
@@ -996,6 +997,15 @@ function buildStartPlan(hostId, message, sessionId) {
|
|
|
996
997
|
env: browser.env,
|
|
997
998
|
};
|
|
998
999
|
}
|
|
1000
|
+
function buildInteractiveResumeCommand(hostId, sessionId) {
|
|
1001
|
+
if (hostId === 'codex')
|
|
1002
|
+
return `codex resume ${sessionId}`;
|
|
1003
|
+
if (hostId === 'gemini')
|
|
1004
|
+
return `gemini --resume ${sessionId}`;
|
|
1005
|
+
if (hostId === 'copilot')
|
|
1006
|
+
return `copilot --resume ${sessionId}`;
|
|
1007
|
+
return `claude -r ${sessionId}`;
|
|
1008
|
+
}
|
|
999
1009
|
function buildContinuePlan(hostId, sessionId, message) {
|
|
1000
1010
|
if (hostId === 'codex') {
|
|
1001
1011
|
const browser = sharedBrowserHostConfig('codex');
|
|
@@ -21,6 +21,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
21
21
|
const os_1 = __importDefault(require("os"));
|
|
22
22
|
const child_process_1 = require("child_process");
|
|
23
23
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
24
|
+
const hub_release_download_1 = require("./hub-release-download");
|
|
24
25
|
// Stable install directory under ~/.fraim/bin/fraim-hub-electron/
|
|
25
26
|
function resolveHubInstallDir() {
|
|
26
27
|
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'bin', 'fraim-hub-electron');
|
|
@@ -125,10 +126,9 @@ async function runHubInstall() {
|
|
|
125
126
|
const target = resolveShortcutTarget();
|
|
126
127
|
const home = os_1.default.homedir();
|
|
127
128
|
const shortcuts = getOsShortcutPaths(home);
|
|
128
|
-
// Ensure the install directory exists (the actual Electron binary download
|
|
129
|
-
// is a separate step — this wires the OS launcher to the expected path so
|
|
130
|
-
// the shortcut is ready as soon as the binary is downloaded).
|
|
131
129
|
fs_1.default.mkdirSync(installDir, { recursive: true });
|
|
130
|
+
const downloaded = await (0, hub_release_download_1.downloadLatestHubBinary)(installDir, target);
|
|
131
|
+
console.log(` Downloaded release asset: ${downloaded.assetName}`);
|
|
132
132
|
if (process.platform === 'win32' && shortcuts.startMenu) {
|
|
133
133
|
fs_1.default.mkdirSync(path_1.default.dirname(shortcuts.startMenu), { recursive: true });
|
|
134
134
|
writeWindowsShortcutScript(shortcuts.startMenu, target);
|
|
@@ -155,9 +155,6 @@ async function runHubInstall() {
|
|
|
155
155
|
console.log('');
|
|
156
156
|
console.log('FRAIM Hub is registered as an OS application.');
|
|
157
157
|
console.log('Launch it from your Start Menu / Launchpad / application launcher.');
|
|
158
|
-
console.log('');
|
|
159
|
-
console.log('Note: the Hub binary will be downloaded on first launch via:');
|
|
160
|
-
console.log(' fraim hub (or npx fraim-hub@latest)');
|
|
161
158
|
}
|
|
162
159
|
exports.hubInstallCommand = new commander_1.Command('install')
|
|
163
160
|
.description('Register FRAIM Hub as an OS application (Start Menu / Launchpad entry)')
|
|
@@ -0,0 +1,123 @@
|
|
|
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.selectHubReleaseAsset = selectHubReleaseAsset;
|
|
7
|
+
exports.downloadLatestHubBinary = downloadLatestHubBinary;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const promises_1 = require("stream/promises");
|
|
12
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
13
|
+
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/mathursrus/FRAIM/releases/latest';
|
|
14
|
+
function archNeedles(arch) {
|
|
15
|
+
if (arch === 'x64')
|
|
16
|
+
return ['x64', 'x86_64', 'amd64'];
|
|
17
|
+
if (arch === 'arm64')
|
|
18
|
+
return ['arm64', 'aarch64'];
|
|
19
|
+
return [arch.toLowerCase()];
|
|
20
|
+
}
|
|
21
|
+
function nameIncludesArch(name, arch) {
|
|
22
|
+
const lower = name.toLowerCase();
|
|
23
|
+
const needles = archNeedles(arch);
|
|
24
|
+
return needles.some((needle) => lower.includes(needle)) || !/(x64|x86_64|amd64|arm64|aarch64)/i.test(name);
|
|
25
|
+
}
|
|
26
|
+
function selectHubReleaseAsset(assets, platform = process.platform, arch = process.arch) {
|
|
27
|
+
const candidates = assets.filter((asset) => /fraim[-_ ]?hub/i.test(asset.name) && nameIncludesArch(asset.name, arch));
|
|
28
|
+
const preferredPatterns = platform === 'win32'
|
|
29
|
+
? [/portable.*\.exe$/i, /\.exe$/i, /\.msi$/i]
|
|
30
|
+
: platform === 'darwin'
|
|
31
|
+
? [/mac.*\.zip$/i, /\.zip$/i, /\.dmg$/i]
|
|
32
|
+
: [/\.AppImage$/i, /\.deb$/i];
|
|
33
|
+
for (const pattern of preferredPatterns) {
|
|
34
|
+
const match = candidates.find((asset) => pattern.test(asset.name));
|
|
35
|
+
if (match)
|
|
36
|
+
return match;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function assertHttpsUrl(rawUrl) {
|
|
41
|
+
const url = new URL(rawUrl);
|
|
42
|
+
if (url.protocol !== 'https:') {
|
|
43
|
+
throw new Error(`Refusing non-HTTPS Hub release URL: ${rawUrl}`);
|
|
44
|
+
}
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
function getJson(rawUrl) {
|
|
48
|
+
const url = assertHttpsUrl(rawUrl);
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const req = https_1.default.get(url, { headers: { 'User-Agent': 'fraim-hub-install' }, timeout: 15000 }, (res) => {
|
|
51
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
52
|
+
res.resume();
|
|
53
|
+
getJson(res.headers.location).then(resolve, reject);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (res.statusCode !== 200) {
|
|
57
|
+
res.resume();
|
|
58
|
+
reject(new Error(`GitHub release lookup failed with HTTP ${res.statusCode}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let body = '';
|
|
62
|
+
res.setEncoding('utf8');
|
|
63
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
resolve(JSON.parse(body));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
req.on('timeout', () => {
|
|
75
|
+
req.destroy(new Error('Timed out while looking up the latest FRAIM Hub release'));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function downloadFile(rawUrl, destination, redirectsRemaining = 5) {
|
|
80
|
+
const url = assertHttpsUrl(rawUrl);
|
|
81
|
+
await new Promise((resolve, reject) => {
|
|
82
|
+
const req = https_1.default.get(url, { headers: { 'User-Agent': 'fraim-hub-install' }, timeout: 60000 }, (res) => {
|
|
83
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsRemaining > 0) {
|
|
84
|
+
res.resume();
|
|
85
|
+
downloadFile(res.headers.location, destination, redirectsRemaining - 1).then(resolve, reject);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (res.statusCode !== 200) {
|
|
89
|
+
res.resume();
|
|
90
|
+
reject(new Error(`Hub release download failed with HTTP ${res.statusCode}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
94
|
+
void (0, promises_1.pipeline)(res, fs_1.default.createWriteStream(destination)).then(resolve, reject);
|
|
95
|
+
});
|
|
96
|
+
req.on('error', reject);
|
|
97
|
+
req.on('timeout', () => {
|
|
98
|
+
req.destroy(new Error('Timed out while downloading the FRAIM Hub release asset'));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function installDownloadedAsset(downloadPath, assetName, installDir, target) {
|
|
103
|
+
if (process.platform === 'darwin' && /\.zip$/i.test(assetName)) {
|
|
104
|
+
fs_1.default.rmSync(path_1.default.join(installDir, 'FRAIM Hub.app'), { recursive: true, force: true });
|
|
105
|
+
new adm_zip_1.default(downloadPath).extractAllTo(installDir, true);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
fs_1.default.copyFileSync(downloadPath, target);
|
|
109
|
+
if (process.platform !== 'win32') {
|
|
110
|
+
fs_1.default.chmodSync(target, 0o755);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function downloadLatestHubBinary(installDir, target) {
|
|
114
|
+
const release = await getJson(process.env.FRAIM_HUB_RELEASE_API_URL || LATEST_RELEASE_API_URL);
|
|
115
|
+
const asset = selectHubReleaseAsset(release.assets || []);
|
|
116
|
+
if (!asset) {
|
|
117
|
+
throw new Error('No runnable FRAIM Hub release asset was found for this platform.');
|
|
118
|
+
}
|
|
119
|
+
const downloadPath = path_1.default.join(installDir, asset.name);
|
|
120
|
+
await downloadFile(asset.browser_download_url, downloadPath);
|
|
121
|
+
installDownloadedAsset(downloadPath, asset.name, installDir, target);
|
|
122
|
+
return { assetName: asset.name, target };
|
|
123
|
+
}
|
|
@@ -24,23 +24,30 @@ class HttpHubRemoteGateway {
|
|
|
24
24
|
timeout: 10000,
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
async
|
|
27
|
+
async getPersonaStateResult(apiKey) {
|
|
28
|
+
// No key: a definitive "unauthenticated" answer — the authority is not the problem.
|
|
28
29
|
if (!apiKey)
|
|
29
|
-
return null;
|
|
30
|
+
return { state: null, reachable: true };
|
|
30
31
|
try {
|
|
31
32
|
const res = await this.client(apiKey).get('/api/personas/me');
|
|
32
|
-
return res.data;
|
|
33
|
+
return { state: res.data, reachable: true };
|
|
33
34
|
}
|
|
34
35
|
catch (err) {
|
|
35
|
-
// 401 (no/expired key) or 404 (feature disabled)
|
|
36
|
-
//
|
|
36
|
+
// 401 (no/expired key) or 404 (feature disabled) are definitive answers from a
|
|
37
|
+
// reachable authority => treat as "no state" so the Hub renders a clean
|
|
38
|
+
// locked/not-signed-in view instead of crashing.
|
|
37
39
|
const status = err?.response?.status;
|
|
38
40
|
if (status === 401 || status === 404)
|
|
39
|
-
return null;
|
|
41
|
+
return { state: null, reachable: true };
|
|
42
|
+
// Anything else (timeout, connection refused, 5xx) means we could not reach the
|
|
43
|
+
// authority. Issue #925: the caller must not treat this like a signed-out user.
|
|
40
44
|
console.warn('[ai-hub] getPersonaState failed:', err?.message || err);
|
|
41
|
-
return null;
|
|
45
|
+
return { state: null, reachable: false };
|
|
42
46
|
}
|
|
43
47
|
}
|
|
48
|
+
async getPersonaState(apiKey) {
|
|
49
|
+
return (await this.getPersonaStateResult(apiKey)).state;
|
|
50
|
+
}
|
|
44
51
|
async listManagerTeam(apiKey) {
|
|
45
52
|
if (!apiKey)
|
|
46
53
|
return [];
|