fraim-hub 2.0.238 → 2.0.239
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 -12
- package/dist/src/ai-hub/hosts.js +262 -11
- package/dist/src/ai-hub/hub2-remote-config.js +3 -3
- package/dist/src/ai-hub/preferences.js +30 -1
- package/dist/src/ai-hub/server.js +520 -21
- package/dist/src/ai-hub/word-sideload.js +95 -0
- package/dist/src/config/persona-capability-bundles.js +2 -2
- package/extensions/office-word/taskpane.html +6 -6
- package/index.js +1 -1
- package/package.json +2 -2
- package/public/ai-hub/index.html +5 -5
- 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 -63
- package/public/ai-hub/script.js +271 -107
- package/public/ai-hub/styles.css +51 -21
- 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/bin/fraim-hub-2.js
CHANGED
|
@@ -1,12 +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
|
-
}
|
|
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/hosts.js
CHANGED
|
@@ -8,6 +8,12 @@ exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
|
|
|
8
8
|
exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
|
|
9
9
|
exports.parseUsageSignal = parseUsageSignal;
|
|
10
10
|
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
11
|
+
exports.__setAgentAvailabilityPathForTests = __setAgentAvailabilityPathForTests;
|
|
12
|
+
exports.invalidateEmployeeDetectionCache = invalidateEmployeeDetectionCache;
|
|
13
|
+
exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
|
|
14
|
+
exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
|
|
15
|
+
exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
|
|
16
|
+
exports.detectEmployeesAsync = detectEmployeesAsync;
|
|
11
17
|
exports.detectEmployees = detectEmployees;
|
|
12
18
|
exports.prepareCodexBrowserHome = prepareCodexBrowserHome;
|
|
13
19
|
exports.sharedBrowserHostConfig = sharedBrowserHostConfig;
|
|
@@ -724,14 +730,246 @@ function resolveHostInvocation(plan) {
|
|
|
724
730
|
args: ['/d', '/s', '/c', [command, ...args.map(escapeWindowsArg)].join(' ')],
|
|
725
731
|
};
|
|
726
732
|
}
|
|
733
|
+
// Single source for the probe environment, shared by the sync and async probes so the two
|
|
734
|
+
// cannot drift on how the managed-agent bin directories are put on PATH.
|
|
735
|
+
const versionProbeEnv = () => ({
|
|
736
|
+
...process.env,
|
|
737
|
+
PATH: (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH),
|
|
738
|
+
});
|
|
727
739
|
const availableByVersionProbe = (command) => {
|
|
728
740
|
const invocation = resolveHostInvocation({ command, args: ['--version'] });
|
|
729
741
|
const result = (0, child_process_1.spawnSync)(invocation.command, invocation.args, {
|
|
730
742
|
encoding: 'utf8',
|
|
731
|
-
env:
|
|
743
|
+
env: versionProbeEnv(),
|
|
732
744
|
});
|
|
733
745
|
return result.status === 0;
|
|
734
746
|
};
|
|
747
|
+
// Issue #1010: the async counterpart of availableByVersionProbe. Same semantics (exit 0
|
|
748
|
+
// from `<agent> --version` means the CLI is installed AND actually runs), but non-blocking
|
|
749
|
+
// so N agents can be probed concurrently without freezing the event loop.
|
|
750
|
+
// Upper bound on a single probe. Bug-bash finding: `spawn`/`spawnSync` here are otherwise
|
|
751
|
+
// unbounded, so one wedged CLI would stall detection forever - and because the first-paint
|
|
752
|
+
// path awaits detection, that would turn the slow first paint this issue is about into an
|
|
753
|
+
// indefinite one. The worst probe observed on a healthy machine is 1419ms (gemini), so 10s
|
|
754
|
+
// is far above any legitimate `--version`. The tradeoff is deliberate: a CLI that cannot
|
|
755
|
+
// print its version within 10s is reported unavailable rather than allowed to hang the Hub.
|
|
756
|
+
// It is recoverable from the UI via the existing per-agent "Check" action.
|
|
757
|
+
const VERSION_PROBE_TIMEOUT_MS = 10_000;
|
|
758
|
+
const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
759
|
+
const invocation = resolveHostInvocation({ command, args: ['--version'] });
|
|
760
|
+
let settled = false;
|
|
761
|
+
let timer;
|
|
762
|
+
let child;
|
|
763
|
+
const finish = (value) => {
|
|
764
|
+
if (settled)
|
|
765
|
+
return;
|
|
766
|
+
settled = true;
|
|
767
|
+
if (timer)
|
|
768
|
+
clearTimeout(timer);
|
|
769
|
+
resolve(value);
|
|
770
|
+
};
|
|
771
|
+
try {
|
|
772
|
+
child = (0, child_process_1.spawn)(invocation.command, invocation.args, {
|
|
773
|
+
env: versionProbeEnv(),
|
|
774
|
+
stdio: 'ignore',
|
|
775
|
+
});
|
|
776
|
+
timer = setTimeout(() => {
|
|
777
|
+
console.warn(`[ai-hub] agent version probe timed out after ${VERSION_PROBE_TIMEOUT_MS}ms: ${command}`);
|
|
778
|
+
try {
|
|
779
|
+
child?.kill();
|
|
780
|
+
}
|
|
781
|
+
catch { /* already gone */ }
|
|
782
|
+
finish(false);
|
|
783
|
+
}, VERSION_PROBE_TIMEOUT_MS);
|
|
784
|
+
// Do not hold the process open just for a probe.
|
|
785
|
+
timer.unref?.();
|
|
786
|
+
child.on('error', () => finish(false));
|
|
787
|
+
child.on('close', (code) => finish(code === 0));
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
finish(false);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
// ─── Issue #1010: employee-detection cache ───────────────────────────────────
|
|
794
|
+
//
|
|
795
|
+
// detectEmployees() used to run one blocking spawnSync per agent on EVERY call, and it is
|
|
796
|
+
// called from bootstrapResponse and several routes. Measured 2715ms per call versus 53ms
|
|
797
|
+
// for all of the Hub's own local discovery in the same handler (51x), with gemini (1419ms)
|
|
798
|
+
// and copilot (939ms) dominating. Worse than the latency: spawnSync blocks Node's event
|
|
799
|
+
// loop, so the Hub answered NOTHING while probing. A trivial 3ms endpoint measured 3043ms
|
|
800
|
+
// when it happened to land during a bootstrap.
|
|
801
|
+
//
|
|
802
|
+
// "Which CLIs are installed on this machine" does not change between requests, so it is
|
|
803
|
+
// cached. The TTL is a safety net for a CLI installed outside the Hub; installs performed
|
|
804
|
+
// THROUGH the Hub invalidate explicitly (see the install-agent route) so they appear at once.
|
|
805
|
+
const EMPLOYEE_DETECTION_TTL_MS = 5 * 60 * 1000;
|
|
806
|
+
let employeeDetectionTtlMs = EMPLOYEE_DETECTION_TTL_MS;
|
|
807
|
+
let cachedEmployees = null;
|
|
808
|
+
let cachedEmployeesAtMs = 0;
|
|
809
|
+
let inFlightDetection = null;
|
|
810
|
+
function cachedEmployeesIfFresh() {
|
|
811
|
+
if (!cachedEmployees)
|
|
812
|
+
return null;
|
|
813
|
+
if (Date.now() - cachedEmployeesAtMs > employeeDetectionTtlMs)
|
|
814
|
+
return null;
|
|
815
|
+
return cachedEmployees;
|
|
816
|
+
}
|
|
817
|
+
// ─── Persisted last-known availability ───────────────────────────────────────
|
|
818
|
+
//
|
|
819
|
+
// An in-memory cache alone does not make the FIRST paint fast: after a Hub restart the
|
|
820
|
+
// cache is empty, so the first bootstrap pays the full probe. Measured, the parallel probe
|
|
821
|
+
// takes a consistent ~2.2s, and a headless browser hitting a freshly started server beat
|
|
822
|
+
// startup priming and paid 2529ms - i.e. whether first paint is fast came down to a race
|
|
823
|
+
// between Electron boot and priming. That is not a guarantee.
|
|
824
|
+
//
|
|
825
|
+
// Agent availability changes rarely (installing a CLI is a deliberate act), so the last
|
|
826
|
+
// known answer is persisted and served immediately on the next start. Startup priming then
|
|
827
|
+
// force-refreshes in the background.
|
|
828
|
+
//
|
|
829
|
+
// Tradeoff, deliberate: immediately after uninstalling a CLI outside the Hub, the first
|
|
830
|
+
// paint can briefly show it as available until the background refresh lands (~2.2s) or the
|
|
831
|
+
// TTL lapses. That is recoverable and self-correcting, and is preferable to freezing every
|
|
832
|
+
// cold start for 2.2s. Installs performed THROUGH the Hub invalidate explicitly.
|
|
833
|
+
const AGENT_AVAILABILITY_FILE = 'hub-agent-availability.json';
|
|
834
|
+
// Overridable so tests do not mutate the developer's real ~/.fraim state. These guards run
|
|
835
|
+
// in the smoke suite now, and invalidation DELETES this file, so pointing them at a temp
|
|
836
|
+
// path keeps a frequent test run from repeatedly clearing real agent-availability data.
|
|
837
|
+
let agentAvailabilityPathOverride = null;
|
|
838
|
+
/** Test seam only. Pass null to restore the real user-level path. */
|
|
839
|
+
function __setAgentAvailabilityPathForTests(filePath) {
|
|
840
|
+
agentAvailabilityPathOverride = filePath;
|
|
841
|
+
}
|
|
842
|
+
function agentAvailabilityFilePath() {
|
|
843
|
+
if (agentAvailabilityPathOverride)
|
|
844
|
+
return agentAvailabilityPathOverride;
|
|
845
|
+
// Resolved lazily and defensively: this module is imported by CLI paths that must not
|
|
846
|
+
// fail to load just because a home directory is unusual.
|
|
847
|
+
return path_1.default.join(os_1.default.homedir(), '.fraim', AGENT_AVAILABILITY_FILE);
|
|
848
|
+
}
|
|
849
|
+
function loadPersistedEmployees() {
|
|
850
|
+
try {
|
|
851
|
+
const raw = fs_1.default.readFileSync(agentAvailabilityFilePath(), 'utf8');
|
|
852
|
+
const parsed = JSON.parse(raw);
|
|
853
|
+
if (!Array.isArray(parsed.employees) || parsed.employees.length === 0)
|
|
854
|
+
return null;
|
|
855
|
+
// Only accept entries that still match the current known agent ids, so a stale file
|
|
856
|
+
// from an older build cannot introduce unknown ids into the roster.
|
|
857
|
+
const knownIds = new Set(Object.keys(EMPLOYEE_LABELS));
|
|
858
|
+
const employees = parsed.employees.filter((e) => e && typeof e.id === 'string' && knownIds.has(e.id));
|
|
859
|
+
if (employees.length === 0)
|
|
860
|
+
return null;
|
|
861
|
+
const detectedAtMs = parsed.detectedAt ? Date.parse(parsed.detectedAt) : NaN;
|
|
862
|
+
return { employees, detectedAtMs: Number.isFinite(detectedAtMs) ? detectedAtMs : 0 };
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return null; // absent or malformed is fine; we simply probe instead
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Adopt a persisted entry into the in-memory cache, preserving its ORIGINAL age.
|
|
870
|
+
*
|
|
871
|
+
* Critically this does NOT stamp `Date.now()`. Doing so made a stale file look permanently
|
|
872
|
+
* fresh, which silently killed the TTL: `detectEmployees()` expired the in-memory entry,
|
|
873
|
+
* immediately re-read the same stale file, and never re-probed - so a CLI installed outside
|
|
874
|
+
* the Hub was never picked up. Caught by asserting on the persisted `detectedAt` stamp; a
|
|
875
|
+
* timing-based assertion had masked it.
|
|
876
|
+
*
|
|
877
|
+
* When the adopted entry is already past the TTL it is still returned (first paint stays
|
|
878
|
+
* fast) but a background refresh is kicked off so the next read is correct.
|
|
879
|
+
*/
|
|
880
|
+
function adoptPersistedEmployees(persisted) {
|
|
881
|
+
cachedEmployees = persisted.employees;
|
|
882
|
+
cachedEmployeesAtMs = persisted.detectedAtMs;
|
|
883
|
+
if (Date.now() - persisted.detectedAtMs > employeeDetectionTtlMs) {
|
|
884
|
+
void detectEmployeesAsync({ force: true }).catch(() => undefined);
|
|
885
|
+
}
|
|
886
|
+
return persisted.employees;
|
|
887
|
+
}
|
|
888
|
+
function persistEmployees(employees) {
|
|
889
|
+
try {
|
|
890
|
+
const file = agentAvailabilityFilePath();
|
|
891
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
892
|
+
fs_1.default.writeFileSync(file, JSON.stringify({ employees, detectedAt: new Date().toISOString() }, null, 2), 'utf8');
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
// Best effort only: failing to persist must never break detection.
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function storeDetectedEmployees(employees) {
|
|
899
|
+
cachedEmployees = employees;
|
|
900
|
+
cachedEmployeesAtMs = Date.now();
|
|
901
|
+
persistEmployees(employees);
|
|
902
|
+
return employees;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Drop the cached agent-availability result so the next detection re-probes.
|
|
906
|
+
* Call after anything that changes what is installed (for example the Hub's own
|
|
907
|
+
* install-agent flow) so a newly installed CLI shows up without waiting for the TTL.
|
|
908
|
+
*/
|
|
909
|
+
function invalidateEmployeeDetectionCache() {
|
|
910
|
+
cachedEmployees = null;
|
|
911
|
+
cachedEmployeesAtMs = 0;
|
|
912
|
+
inFlightDetection = null;
|
|
913
|
+
try {
|
|
914
|
+
fs_1.default.rmSync(agentAvailabilityFilePath(), { force: true });
|
|
915
|
+
}
|
|
916
|
+
catch { /* best effort */ }
|
|
917
|
+
}
|
|
918
|
+
/** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
|
|
919
|
+
function __setEmployeeDetectionTtlForTests(ttlMs) {
|
|
920
|
+
employeeDetectionTtlMs = ttlMs ?? EMPLOYEE_DETECTION_TTL_MS;
|
|
921
|
+
}
|
|
922
|
+
// Counts detection rounds that actually executed CLI probes, as opposed to being served
|
|
923
|
+
// from the in-memory cache or the persisted file. This is the perf signal worth asserting
|
|
924
|
+
// on, because it is ENVIRONMENT-INDEPENDENT: how expensive a probe happens to be varies
|
|
925
|
+
// wildly (2715ms on a machine with the CLIs installed, ~44ms on CI where none are), but
|
|
926
|
+
// "how many times did we pay for it" does not. Absolute millisecond thresholds silently
|
|
927
|
+
// pass on unfixed code wherever probes are cheap; this counter cannot.
|
|
928
|
+
let employeeProbeRounds = 0;
|
|
929
|
+
/** Test seam only: number of detection rounds that really probed. */
|
|
930
|
+
function __getEmployeeProbeRoundsForTests() {
|
|
931
|
+
return employeeProbeRounds;
|
|
932
|
+
}
|
|
933
|
+
/** Test seam only. */
|
|
934
|
+
function __resetEmployeeProbeRoundsForTests() {
|
|
935
|
+
employeeProbeRounds = 0;
|
|
936
|
+
}
|
|
937
|
+
function buildEmployeeStatus(id, available) {
|
|
938
|
+
return {
|
|
939
|
+
id,
|
|
940
|
+
label: EMPLOYEE_LABELS[id],
|
|
941
|
+
available,
|
|
942
|
+
detail: available ? 'Installed and ready on this machine.' : 'CLI not detected on this machine.',
|
|
943
|
+
supportsRaw: supportsDirectPath(id),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Non-blocking employee detection. Probes every agent CONCURRENTLY, so the cost is the
|
|
948
|
+
* slowest single probe rather than the sum of all of them, and the event loop stays free
|
|
949
|
+
* for other requests. Result populates the same cache detectEmployees() reads.
|
|
950
|
+
*
|
|
951
|
+
* Concurrent callers share one in-flight probe rather than each starting their own.
|
|
952
|
+
*/
|
|
953
|
+
async function detectEmployeesAsync(options = {}) {
|
|
954
|
+
const fresh = cachedEmployeesIfFresh();
|
|
955
|
+
if (fresh)
|
|
956
|
+
return fresh;
|
|
957
|
+
if (!options.force) {
|
|
958
|
+
// Fast path for the first request after a restart: serve last-known immediately rather
|
|
959
|
+
// than making first paint wait ~2.2s. Startup priming passes force:true to refresh.
|
|
960
|
+
const persisted = loadPersistedEmployees();
|
|
961
|
+
if (persisted)
|
|
962
|
+
return adoptPersistedEmployees(persisted);
|
|
963
|
+
}
|
|
964
|
+
if (inFlightDetection)
|
|
965
|
+
return inFlightDetection;
|
|
966
|
+
const ids = Object.keys(EMPLOYEE_LABELS);
|
|
967
|
+
employeeProbeRounds += 1;
|
|
968
|
+
inFlightDetection = Promise.all(ids.map(async (id) => buildEmployeeStatus(id, await availableByVersionProbeAsync(agentBinaryName(id)))))
|
|
969
|
+
.then((employees) => storeDetectedEmployees(employees))
|
|
970
|
+
.finally(() => { inFlightDetection = null; });
|
|
971
|
+
return inFlightDetection;
|
|
972
|
+
}
|
|
735
973
|
// Resolve the binary name for each agent tool.
|
|
736
974
|
function agentBinaryName(id) {
|
|
737
975
|
if (id === 'copilot')
|
|
@@ -740,17 +978,26 @@ function agentBinaryName(id) {
|
|
|
740
978
|
return AGY_BINARY;
|
|
741
979
|
return executableName(id);
|
|
742
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Synchronous employee detection, served from the Issue #1010 cache when it is fresh.
|
|
983
|
+
*
|
|
984
|
+
* The blocking probe is retained as the cache-miss fallback rather than removed, so this
|
|
985
|
+
* function's contract is unchanged for every existing caller: it still returns a resolved
|
|
986
|
+
* answer, never a placeholder, and needs no "availability unknown" state in the UI. In
|
|
987
|
+
* practice the miss is rare because the server primes the cache asynchronously at startup
|
|
988
|
+
* (see AiHubServer.start), so requests read a warm cache instead of paying ~2.7s.
|
|
989
|
+
*/
|
|
743
990
|
function detectEmployees() {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
991
|
+
const fresh = cachedEmployeesIfFresh();
|
|
992
|
+
if (fresh)
|
|
993
|
+
return fresh;
|
|
994
|
+
// Prefer last-known over a blocking probe: ~0ms instead of ~2.7s of frozen event loop.
|
|
995
|
+
const persisted = loadPersistedEmployees();
|
|
996
|
+
if (persisted)
|
|
997
|
+
return adoptPersistedEmployees(persisted);
|
|
998
|
+
employeeProbeRounds += 1;
|
|
999
|
+
const employees = Object.keys(EMPLOYEE_LABELS).map((id) => buildEmployeeStatus(id, availableByVersionProbe(agentBinaryName(id))));
|
|
1000
|
+
return storeDetectedEmployees(employees);
|
|
754
1001
|
}
|
|
755
1002
|
function parseFraimInvocation(message) {
|
|
756
1003
|
const trimmed = message.trim();
|
|
@@ -1476,6 +1723,10 @@ class CliHostRuntime {
|
|
|
1476
1723
|
detectEmployees() {
|
|
1477
1724
|
return detectEmployees();
|
|
1478
1725
|
}
|
|
1726
|
+
// Issue #1010: probes all agents concurrently without blocking the event loop.
|
|
1727
|
+
detectEmployeesAsync() {
|
|
1728
|
+
return detectEmployeesAsync();
|
|
1729
|
+
}
|
|
1479
1730
|
startRun(hostId, projectPath, message, handlers, sessionId, launchContext) {
|
|
1480
1731
|
return spawnHostProcess(hostId, (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(buildStartPlan(hostId, message, sessionId), launchContext?.agent, launchContext?.env), projectPath, handlers);
|
|
1481
1732
|
}
|
|
@@ -7,9 +7,9 @@ const remote_hub_gateway_1 = require("./remote-hub-gateway");
|
|
|
7
7
|
exports.HUB2_REMOTE_UI_CHANNEL = 'hub2';
|
|
8
8
|
exports.HUB2_REMOTE_UI_RELEASE_ID = '2026.07.25.1';
|
|
9
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-----
|
|
10
|
+
exports.HUB2_REMOTE_UI_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
11
|
+
MCowBQYDK2VwAyEAn53Ks3CQSqzflw4/6KIJbKmZeWy94V5H7LYlWhMOePo=
|
|
12
|
+
-----END PUBLIC KEY-----
|
|
13
13
|
`;
|
|
14
14
|
function hub2RemoteManifestUrl(remoteBaseUrl = (0, remote_hub_gateway_1.resolveFraimRemoteUrl)()) {
|
|
15
15
|
const target = new URL('/api/ai-hub/ui/releases/latest', remoteBaseUrl);
|
|
@@ -134,6 +134,27 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
|
134
134
|
add(normalizeProjectEntry(project));
|
|
135
135
|
return withUniqueProjectIds(Array.from(byPath.values()));
|
|
136
136
|
}
|
|
137
|
+
// Issue #1024: tombstones live in a separate file so a wholesale replacement of
|
|
138
|
+
// ai-hub-state.json (backup restore, cross-machine sync) can never drop project
|
|
139
|
+
// deletions that happened after the snapshot was taken.
|
|
140
|
+
function tombstoneFilePath(stateFilePath) {
|
|
141
|
+
return stateFilePath.replace(/\.json$/i, '-tombstones.json');
|
|
142
|
+
}
|
|
143
|
+
function readTombstoneFile(stateFilePath) {
|
|
144
|
+
const tombFile = tombstoneFilePath(stateFilePath);
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(fs_1.default.readFileSync(tombFile, 'utf8'));
|
|
147
|
+
return normalizeRemovedProjectPaths(raw);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function writeTombstoneFile(stateFilePath, paths) {
|
|
154
|
+
const tombFile = tombstoneFilePath(stateFilePath);
|
|
155
|
+
fs_1.default.mkdirSync(path_1.default.dirname(tombFile), { recursive: true });
|
|
156
|
+
fs_1.default.writeFileSync(tombFile, JSON.stringify(normalizeRemovedProjectPaths(paths), null, 2));
|
|
157
|
+
}
|
|
137
158
|
class AiHubPreferencesStore {
|
|
138
159
|
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-state.json')) {
|
|
139
160
|
this.stateFilePath = stateFilePath;
|
|
@@ -144,7 +165,13 @@ class AiHubPreferencesStore {
|
|
|
144
165
|
}
|
|
145
166
|
try {
|
|
146
167
|
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
147
|
-
|
|
168
|
+
// Issue #1024: merge tombstones from ai-hub-state.json with those in the
|
|
169
|
+
// separate tombstone file so a wholesale restore of the state file can never
|
|
170
|
+
// drop project deletions that happened after the snapshot was taken.
|
|
171
|
+
const removedProjectPaths = normalizeRemovedProjectPaths([
|
|
172
|
+
...(Array.isArray(raw.removedProjectPaths) ? raw.removedProjectPaths : []),
|
|
173
|
+
...readTombstoneFile(this.stateFilePath),
|
|
174
|
+
]);
|
|
148
175
|
return {
|
|
149
176
|
projectPath: raw.projectPath || projectPath,
|
|
150
177
|
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot' || raw.employeeId === 'antigravity') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
@@ -168,6 +195,8 @@ class AiHubPreferencesStore {
|
|
|
168
195
|
save(preferences) {
|
|
169
196
|
fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
|
|
170
197
|
fs_1.default.writeFileSync(this.stateFilePath, JSON.stringify(preferences, null, 2));
|
|
198
|
+
// Issue #1024: keep the tombstone file in sync so load() always sees the union.
|
|
199
|
+
writeTombstoneFile(this.stateFilePath, preferences.removedProjectPaths || []);
|
|
171
200
|
}
|
|
172
201
|
saveProjects(projectPath, projects, options = {}) {
|
|
173
202
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|