fraim-framework 2.0.192 → 2.0.193

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.
@@ -19,6 +19,7 @@ const defaultPreferences = (projectPath) => ({
19
19
  recentJobInstructions: {},
20
20
  personaKey: null,
21
21
  projects: normalizeAiHubProjectList([], projectPath),
22
+ removedProjectPaths: [],
22
23
  });
23
24
  function normalizeProjectPath(projectPath) {
24
25
  return path_1.default.resolve(projectPath || process.cwd());
@@ -38,6 +39,21 @@ function projectPathExists(projectPath) {
38
39
  function projectIdForPath(projectPath) {
39
40
  return `p-${(0, crypto_1.createHash)('sha1').update(canonicalProjectPath(projectPath)).digest('base64url').slice(0, 16)}`;
40
41
  }
42
+ function normalizeRemovedProjectPaths(raw, currentProjectPath) {
43
+ if (!Array.isArray(raw))
44
+ return [];
45
+ const currentKey = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
46
+ const seen = new Set();
47
+ for (const value of raw) {
48
+ if (typeof value !== 'string' || value.trim().length === 0)
49
+ continue;
50
+ const key = canonicalProjectPath(value);
51
+ if (currentKey && key === currentKey)
52
+ continue;
53
+ seen.add(key);
54
+ }
55
+ return Array.from(seen);
56
+ }
41
57
  function uniqueProjectId(entry, seenIds) {
42
58
  const fallbackId = projectIdForPath(entry.folderPath);
43
59
  const rawId = typeof entry.id === 'string' ? entry.id.trim() : '';
@@ -86,10 +102,13 @@ function normalizeProjectEntry(raw, fallbackPath) {
86
102
  function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
87
103
  const byPath = new Map();
88
104
  const currentKey = currentProjectPath ? normalizeProjectPath(currentProjectPath) : null;
105
+ const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths, currentProjectPath));
89
106
  const add = (entry) => {
90
107
  if (!entry)
91
108
  return;
92
109
  const key = normalizeProjectPath(entry.folderPath);
110
+ if (removedKeys.has(canonicalProjectPath(key)))
111
+ return;
93
112
  if (!options.includeMissing && key !== currentKey && !projectPathExists(key))
94
113
  return;
95
114
  const existing = byPath.get(key);
@@ -111,6 +130,7 @@ class AiHubPreferencesStore {
111
130
  }
112
131
  try {
113
132
  const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
133
+ const removedProjectPaths = normalizeRemovedProjectPaths(raw.removedProjectPaths, projectPath);
114
134
  return {
115
135
  projectPath: raw.projectPath || projectPath,
116
136
  employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot') ? raw.employeeId : DEFAULT_EMPLOYEE,
@@ -121,7 +141,8 @@ class AiHubPreferencesStore {
121
141
  : {},
122
142
  personaKey: typeof raw.personaKey === 'string' ? raw.personaKey : null,
123
143
  apiKey: typeof raw.apiKey === 'string' && raw.apiKey.length > 0 ? raw.apiKey : undefined,
124
- projects: normalizeAiHubProjectList(Array.isArray(raw.projects) ? raw.projects : [], projectPath),
144
+ projects: normalizeAiHubProjectList(Array.isArray(raw.projects) ? raw.projects : [], projectPath, { removedProjectPaths }),
145
+ removedProjectPaths,
125
146
  };
126
147
  }
127
148
  catch {
@@ -132,18 +153,30 @@ class AiHubPreferencesStore {
132
153
  fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
133
154
  fs_1.default.writeFileSync(this.stateFilePath, JSON.stringify(preferences, null, 2));
134
155
  }
135
- saveProjects(projectPath, projects) {
156
+ saveProjects(projectPath, projects, options = {}) {
136
157
  const normalizedProjectPath = normalizeProjectPath(projectPath);
137
158
  const preferences = this.load(normalizedProjectPath);
138
- const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath);
139
- this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects });
159
+ const reviveKeys = new Set(normalizeRemovedProjectPaths(options.reviveRemovedPaths, normalizedProjectPath));
160
+ const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths, normalizedProjectPath)
161
+ .filter((removedPath) => !reviveKeys.has(removedPath));
162
+ const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
163
+ this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
140
164
  return nextProjects;
141
165
  }
142
166
  mergeProjects(projectPath, projects) {
143
167
  const normalizedProjectPath = normalizeProjectPath(projectPath);
144
168
  const preferences = this.load(normalizedProjectPath);
145
- const nextProjects = normalizeAiHubProjectList([...(preferences.projects || []), ...projects], normalizedProjectPath);
146
- this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects });
169
+ const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths, normalizedProjectPath);
170
+ const nextProjects = normalizeAiHubProjectList([...(preferences.projects || []), ...projects], normalizedProjectPath, { removedProjectPaths });
171
+ this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
172
+ return nextProjects;
173
+ }
174
+ removeProject(projectPath, projects, removedProjectPath) {
175
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
176
+ const preferences = this.load(normalizedProjectPath);
177
+ const removedProjectPaths = normalizeRemovedProjectPaths([...(preferences.removedProjectPaths || []), removedProjectPath], normalizedProjectPath);
178
+ const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
179
+ this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
147
180
  return nextProjects;
148
181
  }
149
182
  remember(preferences, jobId, instructions) {
@@ -1382,10 +1382,7 @@ class AiHubServer {
1382
1382
  ...(preferences.projects || []),
1383
1383
  ...conversationProjects,
1384
1384
  ...extras,
1385
- ], normalizedProjectPath);
1386
- }
1387
- saveKnownProjects(projectPath, projects) {
1388
- return this.preferencesStore.saveProjects(path_1.default.resolve(projectPath || this.projectPath), this.knownProjects(projectPath, projects));
1385
+ ], normalizedProjectPath, { removedProjectPaths: preferences.removedProjectPaths || [] });
1389
1386
  }
1390
1387
  async bootstrapResponse(projectPath, apiKey) {
1391
1388
  const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
@@ -2270,7 +2267,7 @@ class AiHubServer {
2270
2267
  if (!Array.isArray(body.projects)) {
2271
2268
  return res.status(400).json({ error: 'projects array required' });
2272
2269
  }
2273
- const projects = this.saveKnownProjects(projectPath, body.projects);
2270
+ const projects = this.preferencesStore.saveProjects(projectPath, [...this.knownProjects(projectPath), ...body.projects], { reviveRemovedPaths: Array.isArray(body.reviveRemovedPaths) ? body.reviveRemovedPaths : [] });
2274
2271
  return res.json({ projectPath, projects, source: 'disk' });
2275
2272
  }
2276
2273
  catch (error) {
@@ -2312,9 +2309,9 @@ class AiHubServer {
2312
2309
  // Delete the conversation-store KEY — an empty list would still re-derive
2313
2310
  // the project via listProjectPaths (R9).
2314
2311
  this.conversationStore.removeProject(entry.folderPath);
2315
- // Persist the filtered list directly: saveKnownProjects unions with the
2316
- // derived list by design and cannot express removal.
2317
- const projects = this.preferencesStore.saveProjects(projectPath, known.filter((project) => project.id !== entry.id));
2312
+ // Persist the filtered list with a tombstone: a normal project save
2313
+ // still merges derived projects and cannot express removal.
2314
+ const projects = this.preferencesStore.removeProject(projectPath, known.filter((project) => project.id !== entry.id), entry.folderPath);
2318
2315
  return res.json({ ok: true, projects });
2319
2316
  });
2320
2317
  this.app.post('/api/ai-hub/api-key', (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.192",
3
+ "version": "2.0.193",
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": {
@@ -1011,6 +1011,21 @@ function aggregateDotClass(dots) {
1011
1011
 
1012
1012
  function renderRail() {
1013
1013
  renderTeamRoster();
1014
+ const activeRailProjectKey = (typeof tf !== 'undefined' && tf && tf.activeProjectId)
1015
+ || state.projectPath
1016
+ || 'current';
1017
+ const employeeGroupOpenState = new Map();
1018
+ els['conv-list'].querySelectorAll('details.conv-employee-group[data-employee-key]').forEach((details) => {
1019
+ const key = details.dataset ? details.dataset.employeeKey : '';
1020
+ const projectKey = details.dataset ? (details.dataset.projectKey || activeRailProjectKey) : activeRailProjectKey;
1021
+ if (key) employeeGroupOpenState.set(projectKey + ':' + key, details.open);
1022
+ });
1023
+ const employeeGroupStateKey = (key) => activeRailProjectKey + ':' + key;
1024
+ const preservedEmployeeGroupOpen = (key) => (
1025
+ employeeGroupOpenState.has(employeeGroupStateKey(key))
1026
+ ? employeeGroupOpenState.get(employeeGroupStateKey(key))
1027
+ : true
1028
+ );
1014
1029
  els['conv-list'].innerHTML = '';
1015
1030
  // R4: filter by selected persona when one is active.
1016
1031
  // #693 R1: the "Project Updates" section was retired; project-lifecycle runs
@@ -1167,7 +1182,9 @@ function renderRail() {
1167
1182
  for (const group of groups.values()) {
1168
1183
  const details = document.createElement('details');
1169
1184
  details.className = 'conv-employee-group';
1170
- details.open = true;
1185
+ details.dataset.employeeKey = group.key;
1186
+ details.dataset.projectKey = activeRailProjectKey;
1187
+ details.open = preservedEmployeeGroupOpen(group.key);
1171
1188
  const summary = document.createElement('summary');
1172
1189
  summary.className = 'conv-employee-tab';
1173
1190
  const avatar = buildConversationEmployeeAvatar(group.sample, 'conv-employee-avatar');
@@ -1237,7 +1254,9 @@ function renderRail() {
1237
1254
  if (watercoolerConvs.length > 0) {
1238
1255
  const wcDetails = document.createElement('details');
1239
1256
  wcDetails.className = 'conv-employee-group conv-employee-group--adhoc';
1240
- wcDetails.open = true;
1257
+ wcDetails.dataset.employeeKey = '__watercooler__';
1258
+ wcDetails.dataset.projectKey = activeRailProjectKey;
1259
+ wcDetails.open = preservedEmployeeGroupOpen('__watercooler__');
1241
1260
  const wcSummary = document.createElement('summary');
1242
1261
  wcSummary.className = 'conv-employee-tab';
1243
1262
  // #693.8: watercooler icon (replaces the blank dashed placeholder).
@@ -6400,13 +6419,14 @@ function tfLoadStorage() {
6400
6419
  try { tf.assignments = JSON.parse(window.localStorage.getItem(STORAGE_KEY_ASSIGNMENTS_512) || '{}'); }
6401
6420
  catch { tf.assignments = {}; }
6402
6421
  }
6403
- function tfPersistProjects() {
6422
+ function tfPersistProjects(options) {
6404
6423
  try { window.localStorage.setItem(STORAGE_KEY_PROJECTS_512, JSON.stringify(tf.projects)); } catch {}
6405
6424
  if (!state.projectPath) return;
6425
+ const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
6406
6426
  requestJson('/api/ai-hub/projects', {
6407
6427
  method: 'PUT',
6408
6428
  headers: { 'Content-Type': 'application/json' },
6409
- body: JSON.stringify({ projectPath: state.projectPath, projects: tf.projects }),
6429
+ body: JSON.stringify({ projectPath: state.projectPath, projects: tf.projects, reviveRemovedPaths }),
6410
6430
  }).catch((error) => console.warn('[512] Could not persist projects to local disk:', error));
6411
6431
  }
6412
6432
  function tfPersistAssignments() {
@@ -9400,32 +9420,32 @@ function tfCloseConnectedSurface() {
9400
9420
  tfShowArea(returnArea);
9401
9421
  }
9402
9422
 
9403
- async function tfHandleConnectedAuthMessage(event) {
9404
- const frame = document.getElementById('connected-frame');
9405
- if (frame && frame.contentWindow && event.source !== frame.contentWindow) return;
9406
- let remoteOrigin = '';
9407
- try { remoteOrigin = new URL(tfConnectedRemoteBase()).origin; }
9423
+ async function tfHandleConnectedAuthMessage(event) {
9424
+ const frame = document.getElementById('connected-frame');
9425
+ if (frame && frame.contentWindow && event.source !== frame.contentWindow) return;
9426
+ let remoteOrigin = '';
9427
+ try { remoteOrigin = new URL(tfConnectedRemoteBase()).origin; }
9408
9428
  catch { return; }
9409
9429
  if (event.origin !== remoteOrigin) return;
9410
- const msg = event.data || {};
9411
- if (!msg || msg.type !== 'fraim-auth-api-key' || typeof msg.apiKey !== 'string' || !msg.apiKey) return;
9412
- const previousKey = tfConnectedApiKey();
9413
- await tfRememberConnectedApiKey(msg.apiKey);
9414
- if (state.connectedSurface) {
9415
- const nextUrl = tfConnectedSurfaceUrl(state.connectedSurface);
9416
- const frameEl = document.getElementById('connected-frame');
9417
- if (frameEl) {
9418
- try {
9419
- const current = new URL(frameEl.src || '', window.location.href);
9420
- const next = new URL(nextUrl, window.location.href);
9421
- if (previousKey === msg.apiKey && current.origin === next.origin && current.pathname === next.pathname) {
9422
- return;
9423
- }
9424
- } catch {}
9425
- frameEl.src = nextUrl;
9426
- }
9427
- }
9428
- }
9430
+ const msg = event.data || {};
9431
+ if (!msg || msg.type !== 'fraim-auth-api-key' || typeof msg.apiKey !== 'string' || !msg.apiKey) return;
9432
+ const previousKey = tfConnectedApiKey();
9433
+ await tfRememberConnectedApiKey(msg.apiKey);
9434
+ if (state.connectedSurface) {
9435
+ const nextUrl = tfConnectedSurfaceUrl(state.connectedSurface);
9436
+ const frameEl = document.getElementById('connected-frame');
9437
+ if (frameEl) {
9438
+ try {
9439
+ const current = new URL(frameEl.src || '', window.location.href);
9440
+ const next = new URL(nextUrl, window.location.href);
9441
+ if (previousKey === msg.apiKey && current.origin === next.origin && current.pathname === next.pathname) {
9442
+ return;
9443
+ }
9444
+ } catch {}
9445
+ frameEl.src = nextUrl;
9446
+ }
9447
+ }
9448
+ }
9429
9449
 
9430
9450
  async function tfRememberConnectedApiKey(apiKey) {
9431
9451
  const changed = apiKey !== state.storedApiKey;
@@ -9808,7 +9828,7 @@ async function tfCreateProject() {
9808
9828
  };
9809
9829
  tf.projects.push(project);
9810
9830
  tf.assignments[id] = [];
9811
- tfPersistProjects();
9831
+ tfPersistProjects({ reviveRemovedPaths: [folderPath] });
9812
9832
  tfPersistAssignments();
9813
9833
  tf.activeProjectId = id;
9814
9834
  tfCloseNewProject();