fraim-framework 2.0.201 → 2.0.203
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.
|
@@ -18,7 +18,7 @@ function getFraimVersion() {
|
|
|
18
18
|
if (fs_1.default.existsSync(pkgPath)) {
|
|
19
19
|
try {
|
|
20
20
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
21
|
-
if (pkg.name === 'fraim-framework') {
|
|
21
|
+
if (pkg.name === 'fraim-framework' || pkg.name === 'fraim') {
|
|
22
22
|
return pkg.version;
|
|
23
23
|
}
|
|
24
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.203",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/public/ai-hub/script.js
CHANGED
|
@@ -6439,21 +6439,20 @@ function renderFirstRunLanding(mode) {
|
|
|
6439
6439
|
// Company · Manager · Projects. It ADDS UI around the existing surface and
|
|
6440
6440
|
// removes no existing ids or handlers.
|
|
6441
6441
|
//
|
|
6442
|
-
// Reads the extended bootstrap (firstRun, teamContext, brain, personas).
|
|
6443
|
-
//
|
|
6444
|
-
//
|
|
6445
|
-
//
|
|
6446
|
-
// ===========================================================================
|
|
6447
|
-
|
|
6448
|
-
const
|
|
6449
|
-
const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
|
|
6442
|
+
// Reads the extended bootstrap (firstRun, teamContext, brain, personas).
|
|
6443
|
+
// Project tabs are server-owned local-machine state; per-project employee/job
|
|
6444
|
+
// assignments remain UI-local. The workspace conversation pane reuses the
|
|
6445
|
+
// existing run/poll/coaching code paths verbatim.
|
|
6446
|
+
// ===========================================================================
|
|
6447
|
+
|
|
6448
|
+
const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
|
|
6450
6449
|
|
|
6451
6450
|
const tf = {
|
|
6452
6451
|
enabled: false,
|
|
6453
6452
|
area: 'projects', // company | manager | projects | brain | connected
|
|
6454
6453
|
projectView: 'overview', // overview | workspace
|
|
6455
6454
|
activeProjectId: null,
|
|
6456
|
-
projects: [], // [{ id, name, intent, brief }]
|
|
6455
|
+
projects: [], // server-owned [{ id, name, folderPath, intent, brief }]
|
|
6457
6456
|
assignments: {}, // { [projectId]: [{ id, employeeKey, jobName, jobId }] }
|
|
6458
6457
|
// #726: canonical paths just removed this session. A delete switches workspaces first,
|
|
6459
6458
|
// and that switch's (possibly cached) bootstrap merge would otherwise re-add the project
|
|
@@ -6488,7 +6487,7 @@ function tfPersonaByKey(key) { return tfPersonas().find((p) => p.key === key) ||
|
|
|
6488
6487
|
function tfProjectTeamKeys() { return tfHiredPersonas().map((p) => p.key); }
|
|
6489
6488
|
|
|
6490
6489
|
// ---------------------------------------------------------------------------
|
|
6491
|
-
// Persistence (projects + assignments
|
|
6490
|
+
// Persistence (server-owned projects + client-local assignments)
|
|
6492
6491
|
// ---------------------------------------------------------------------------
|
|
6493
6492
|
function tfCanonicalProjectPath(folderPath) {
|
|
6494
6493
|
const value = String(folderPath || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
@@ -6538,30 +6537,9 @@ function tfApplyUniqueProjectIds(projects) {
|
|
|
6538
6537
|
});
|
|
6539
6538
|
}
|
|
6540
6539
|
|
|
6541
|
-
function
|
|
6542
|
-
const
|
|
6543
|
-
|
|
6544
|
-
const normalized = tfNormalizeProject(project);
|
|
6545
|
-
if (!normalized) return;
|
|
6546
|
-
const key = tfCanonicalProjectPath(normalized.folderPath);
|
|
6547
|
-
// #726: never re-add a project removed this session (stale/cached bootstrap merge).
|
|
6548
|
-
if (tf.removedPaths && tf.removedPaths.has(key)) return;
|
|
6549
|
-
const existing = merged.get(key);
|
|
6550
|
-
if (existing) {
|
|
6551
|
-
const next = { ...existing, ...normalized, folderPath: normalized.folderPath || existing.folderPath };
|
|
6552
|
-
merged.set(key, next);
|
|
6553
|
-
} else {
|
|
6554
|
-
merged.set(key, normalized);
|
|
6555
|
-
}
|
|
6556
|
-
};
|
|
6557
|
-
for (const project of tf.projects || []) add(project);
|
|
6558
|
-
for (const project of projects || []) add(project);
|
|
6559
|
-
tf.projects = tfApplyUniqueProjectIds(Array.from(merged.values()));
|
|
6560
|
-
}
|
|
6561
|
-
|
|
6562
|
-
function tfProjectForPath(folderPath) {
|
|
6563
|
-
const key = tfCanonicalProjectPath(folderPath);
|
|
6564
|
-
return tf.projects.find((project) => tfCanonicalProjectPath(project.folderPath || project.path || project.folder) === key) || null;
|
|
6540
|
+
function tfProjectForPath(folderPath) {
|
|
6541
|
+
const key = tfCanonicalProjectPath(folderPath);
|
|
6542
|
+
return tf.projects.find((project) => tfCanonicalProjectPath(project.folderPath || project.path || project.folder) === key) || null;
|
|
6565
6543
|
}
|
|
6566
6544
|
|
|
6567
6545
|
function tfEnsureCurrentProject() {
|
|
@@ -6576,34 +6554,41 @@ function tfEnsureCurrentProject() {
|
|
|
6576
6554
|
name: friendlyProjectShortName(state.projectPath),
|
|
6577
6555
|
folderPath: state.projectPath,
|
|
6578
6556
|
}, state.projectPath);
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
}
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6557
|
+
let resolved = existingForPath;
|
|
6558
|
+
if (!resolved && current && !(tf.removedPaths && tf.removedPaths.has(tfCanonicalProjectPath(current.folderPath)))) {
|
|
6559
|
+
// Fallback only for the server bootstrap's active project path. General
|
|
6560
|
+
// project-list ownership remains in bootstrap.projects / /api/ai-hub/projects.
|
|
6561
|
+
tf.projects = tfApplyUniqueProjectIds([...(tf.projects || []), current]);
|
|
6562
|
+
resolved = tfProjectForPath(state.projectPath);
|
|
6563
|
+
}
|
|
6564
|
+
if (!tf.activeProjectId || !tf.projects.some((project) => project.id === tf.activeProjectId)) {
|
|
6565
|
+
tf.activeProjectId = resolved ? resolved.id : tf.projects[0]?.id || null;
|
|
6566
|
+
}
|
|
6567
|
+
return resolved;
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
function tfClearLegacyProjectStorage() {
|
|
6571
|
+
try { window.localStorage.removeItem('fraim.aiHub.projects.v1'); } catch {}
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6574
|
+
function tfLoadClientState() {
|
|
6575
|
+
tf.projects = [];
|
|
6576
|
+
tfClearLegacyProjectStorage();
|
|
6577
|
+
try { tf.assignments = JSON.parse(window.localStorage.getItem(STORAGE_KEY_ASSIGNMENTS_512) || '{}'); }
|
|
6578
|
+
catch { tf.assignments = {}; }
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6581
|
+
async function tfSaveProjectsToServer(projects, options) {
|
|
6582
|
+
if (!state.projectPath) throw new Error('No active project path available.');
|
|
6583
|
+
const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
|
|
6584
|
+
const payload = await requestJson('/api/ai-hub/projects', {
|
|
6585
|
+
method: 'PUT',
|
|
6586
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6587
|
+
body: JSON.stringify({ projectPath: state.projectPath, projects: Array.isArray(projects) ? projects : [], reviveRemovedPaths }),
|
|
6588
|
+
});
|
|
6589
|
+
tfAdoptServerProjects(payload && payload.projects);
|
|
6590
|
+
return payload;
|
|
6591
|
+
}
|
|
6607
6592
|
function tfPersistAssignments() {
|
|
6608
6593
|
try { window.localStorage.setItem(STORAGE_KEY_ASSIGNMENTS_512, JSON.stringify(tf.assignments)); } catch {}
|
|
6609
6594
|
}
|
|
@@ -7037,9 +7022,9 @@ function tfConfirmProjectRemove(proj, cardEl) {
|
|
|
7037
7022
|
});
|
|
7038
7023
|
}
|
|
7039
7024
|
|
|
7040
|
-
// R10: adopt a server projects list as the authoritative tf.projects
|
|
7041
|
-
// not a
|
|
7042
|
-
// removed project
|
|
7025
|
+
// R10: adopt a server projects list as the authoritative tf.projects - a REPLACE,
|
|
7026
|
+
// not a client-side union, so stale in-memory/browser state cannot re-add a
|
|
7027
|
+
// removed project.
|
|
7043
7028
|
function tfAdoptServerProjects(projects) {
|
|
7044
7029
|
const normalized = (Array.isArray(projects) ? projects : [])
|
|
7045
7030
|
.map((project) => tfNormalizeProject(project))
|
|
@@ -7099,8 +7084,7 @@ async function tfRemoveProject(proj) {
|
|
|
7099
7084
|
if (!tf.projects.some((p) => p.id === tf.activeProjectId)) {
|
|
7100
7085
|
tf.activeProjectId = tf.projects[0] ? tf.projects[0].id : null;
|
|
7101
7086
|
}
|
|
7102
|
-
|
|
7103
|
-
tfRenderProjectTabs();
|
|
7087
|
+
tfRenderProjectTabs();
|
|
7104
7088
|
tfRenderOverview();
|
|
7105
7089
|
if (typeof renderRail === 'function') renderRail();
|
|
7106
7090
|
showStatus('"' + (proj.name || 'Project') + '" was removed from the Hub. Its files on disk are untouched.', false);
|
|
@@ -10287,26 +10271,36 @@ async function tfCreateProject() {
|
|
|
10287
10271
|
brief: intent,
|
|
10288
10272
|
team: tf.npSelectedEmployees.slice(),
|
|
10289
10273
|
};
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10274
|
+
const removedKey = tfCanonicalProjectPath(folderPath);
|
|
10275
|
+
try {
|
|
10276
|
+
await tfSaveProjectsToServer([...(tf.projects || []), project], { reviveRemovedPaths: [folderPath] });
|
|
10277
|
+
tf.removedPaths.delete(removedKey);
|
|
10278
|
+
} catch (error) {
|
|
10279
|
+
showStatus('Could not create project: ' + (error && error.message ? error.message : 'request failed.'), true);
|
|
10280
|
+
return;
|
|
10281
|
+
}
|
|
10282
|
+
const savedProject = tfProjectForPath(folderPath) || project;
|
|
10283
|
+
if (!tf.assignments[savedProject.id]) tf.assignments[savedProject.id] = [];
|
|
10284
|
+
tfPersistAssignments();
|
|
10285
|
+
tf.activeProjectId = savedProject.id;
|
|
10286
|
+
tfCloseNewProject();
|
|
10287
|
+
tfRenderProjectTabs();
|
|
10288
|
+
|
|
10289
|
+
// Switch the active workspace to this project's folder so all runs,
|
|
10290
|
+
// context reads, and learnings scope to the right place.
|
|
10291
|
+
await tfSwitchProjectFolder(folderPath);
|
|
10292
|
+
const currentProject = tfProjectForPath(folderPath) || savedProject;
|
|
10293
|
+
if (currentProject) {
|
|
10294
|
+
tf.activeProjectId = currentProject.id;
|
|
10295
|
+
if (!tf.assignments[currentProject.id]) tf.assignments[currentProject.id] = [];
|
|
10296
|
+
tfPersistAssignments();
|
|
10297
|
+
}
|
|
10304
10298
|
|
|
10305
10299
|
// #521 fix: re-render the new project's tree + Brief/Learnings top sections so
|
|
10306
10300
|
// they reflect THIS project (the folder switch cleared the project-scoped
|
|
10307
10301
|
// context cache; this re-fetches it) instead of leaving the previous project's
|
|
10308
10302
|
// brief on screen ("random stuff in the brief").
|
|
10309
|
-
tf._lastWorkspaceProject = id;
|
|
10303
|
+
tf._lastWorkspaceProject = currentProject ? currentProject.id : savedProject.id;
|
|
10310
10304
|
tfRenderTree();
|
|
10311
10305
|
tfRenderProjectContextTop();
|
|
10312
10306
|
|
|
@@ -10371,12 +10365,11 @@ async function tfSwitchProjectFolder(folderPath) {
|
|
|
10371
10365
|
} else {
|
|
10372
10366
|
await conversationsPromise;
|
|
10373
10367
|
}
|
|
10374
|
-
|
|
10375
|
-
const currentProject = tfEnsureCurrentProject();
|
|
10376
|
-
if (currentProject) tf.activeProjectId = currentProject.id;
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
}
|
|
10368
|
+
tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
|
|
10369
|
+
const currentProject = tfEnsureCurrentProject();
|
|
10370
|
+
if (currentProject) tf.activeProjectId = currentProject.id;
|
|
10371
|
+
tfRefreshAfterBootstrap();
|
|
10372
|
+
}
|
|
10380
10373
|
|
|
10381
10374
|
// ---------------------------------------------------------------------------
|
|
10382
10375
|
// Assign-job modal (job-first with locks + per-employee filter)
|
|
@@ -10939,20 +10932,19 @@ function tfInitShell() {
|
|
|
10939
10932
|
document.body.classList.add('hub-shell');
|
|
10940
10933
|
const tabs = document.getElementById('hub-tabs');
|
|
10941
10934
|
if (tabs) tabs.hidden = false;
|
|
10942
|
-
|
|
10943
|
-
|
|
10935
|
+
tfLoadClientState();
|
|
10936
|
+
tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
|
|
10944
10937
|
tfWireShell();
|
|
10945
10938
|
tfPopulateAccountMenu(); // #533: show the real account identity, not "SM"
|
|
10946
10939
|
// Seed a project from the loaded folder so the workspace reuses the existing
|
|
10947
10940
|
// conversation machinery for that real project path.
|
|
10948
10941
|
const currentProject = tfEnsureCurrentProject();
|
|
10949
10942
|
if (currentProject && !tf.assignments[currentProject.id]) tf.assignments[currentProject.id] = [];
|
|
10950
|
-
if (currentProject) {
|
|
10951
|
-
tf.activeProjectId = currentProject.id;
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
}
|
|
10943
|
+
if (currentProject) {
|
|
10944
|
+
tf.activeProjectId = currentProject.id;
|
|
10945
|
+
} else if (tf.projects.length && !tf.activeProjectId) {
|
|
10946
|
+
tf.activeProjectId = tf.projects[0].id;
|
|
10947
|
+
}
|
|
10956
10948
|
tfRenderProjectTabs();
|
|
10957
10949
|
tfShowArea('projects');
|
|
10958
10950
|
// Issue #578: wire deployment modal buttons.
|
|
@@ -10974,12 +10966,11 @@ function tfInitShell() {
|
|
|
10974
10966
|
}
|
|
10975
10967
|
|
|
10976
10968
|
// Refresh shell-derived UI after a bootstrap reload (e.g. project picked).
|
|
10977
|
-
function tfRefreshAfterBootstrap() {
|
|
10978
|
-
if (!tf.enabled) return;
|
|
10979
|
-
|
|
10980
|
-
tfEnsureCurrentProject();
|
|
10981
|
-
|
|
10982
|
-
tfRenderProjectTabs();
|
|
10969
|
+
function tfRefreshAfterBootstrap() {
|
|
10970
|
+
if (!tf.enabled) return;
|
|
10971
|
+
tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
|
|
10972
|
+
tfEnsureCurrentProject();
|
|
10973
|
+
tfRenderProjectTabs();
|
|
10983
10974
|
if (tf.area === 'company') tfRenderCompany();
|
|
10984
10975
|
else if (tf.area === 'manager') tfRenderManager();
|
|
10985
10976
|
else if (tf.area === 'brain') tfRenderBrain();
|