fraim-hub 2.0.256 → 2.0.258

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.
@@ -32,7 +32,7 @@ function layerEmployeeReadDirs(layer) {
32
32
  }
33
33
  /** The single writable employees directory for a layer. */
34
34
  function layerEmployeeWriteDir(layer) {
35
- return path_1.default.join((0, pack_home_1.resolvePackHome)(layer).root, EMPLOYEES_SUBDIR);
35
+ return path_1.default.join((0, pack_home_1.resolvePackHome)(layer).contentRoot, EMPLOYEES_SUBDIR);
36
36
  }
37
37
  function readEmployeesFromDir(dir, scope) {
38
38
  if (!fs_1.default.existsSync(dir))
@@ -35,6 +35,7 @@ const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
35
35
  const mcp_config_generator_1 = require("../cli/setup/mcp-config-generator");
36
36
  const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
37
37
  const configured_agents_1 = require("./configured-agents");
38
+ const pack_home_1 = require("../cli/utils/pack-home");
38
39
  // Parse a single line of host stdout looking for a seekMentoring tool-use
39
40
  // signal. Returns null if the line does not contain one. Supports both
40
41
  // hosts FRAIM ships against today:
@@ -1084,10 +1085,10 @@ function transformHeadlessFraimMessage(message, kind) {
1084
1085
  }
1085
1086
  function machineLevelStorageGuard(jobId) {
1086
1087
  const normalized = jobId.toLowerCase();
1087
- const userFraim = path_1.default.join(os_1.default.homedir(), '.fraim');
1088
1088
  if (normalized === 'manager-agreements') {
1089
- const managerContext = path_1.default.join(userFraim, 'personalized-employee', 'context', 'manager_context.md');
1090
- const managerRules = path_1.default.join(userFraim, 'personalized-employee', 'rules', 'manager_rules.md');
1089
+ const managerContentRoot = (0, pack_home_1.resolvePackHome)('manager').contentRoot;
1090
+ const managerContext = path_1.default.join(managerContentRoot, 'context', 'manager_context.md');
1091
+ const managerRules = path_1.default.join(managerContentRoot, 'rules', 'manager_rules.md');
1091
1092
  return [
1092
1093
  'Storage scope guardrail:',
1093
1094
  '- Manager agreements artifacts are machine-level, not repo-level.',
@@ -78,16 +78,18 @@ async function publishOrgArtifacts(artifacts, opts) {
78
78
  throw new Error(`Invalid org-pack deletion path '${rel}'.`);
79
79
  }
80
80
  if (organization.backend === 'local-folder') {
81
- const localPath = organization.localPath;
81
+ // Write into contentRoot (localPath/personalized-employee/) so the read
82
+ // side (packReadRoots -> contentRoot) sees the file immediately.
83
+ const contentRoot = (0, pack_home_1.resolvePackHome)('org').contentRoot;
82
84
  const writtenPaths = [];
83
85
  for (const a of artifacts) {
84
- const result = (0, local_folder_sync_1.writeLocalFolderFile)(localPath, a.relativePath, a.content);
86
+ const result = (0, local_folder_sync_1.writeLocalFolderFile)(contentRoot, a.relativePath, a.content);
85
87
  if (result.skipped) {
86
88
  throw new Error(`Skipped writing '${a.relativePath}' to local folder: ${result.reason} Re-sync before publishing.`);
87
89
  }
88
- writtenPaths.push(path_1.default.join(localPath, a.relativePath));
90
+ writtenPaths.push(path_1.default.join(contentRoot, a.relativePath));
89
91
  }
90
- const deletedPaths = (0, local_folder_sync_1.removeLocalFolderFiles)(localPath, deletions, artifacts.map((a) => a.relativePath));
92
+ const deletedPaths = (0, local_folder_sync_1.removeLocalFolderFiles)(contentRoot, deletions, artifacts.map((a) => a.relativePath));
91
93
  return { backend: 'local-folder', writtenPaths, deletedPaths };
92
94
  }
93
95
  if (organization.backend === 'fraim-cloud') {
@@ -50,18 +50,52 @@ function packsDir() {
50
50
  return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'packs');
51
51
  }
52
52
  /**
53
- * The standard local home, used when the layer is not synced anywhere.
53
+ * The standard local home root (the pack repo root), used when the layer is not
54
+ * synced anywhere.
54
55
  *
55
- * Both layers keep their historical paths so existing machines need no
56
- * migration. Inventing a new location here would orphan content that is
57
- * already on disk: `~/.fraim/org/` has always been where org content lives,
58
- * and `~/.fraim/personalized-employee/` is where a manager authors.
56
+ * For manager: `~/.fraim/` capability content lives at
57
+ * `~/.fraim/personalized-employee/`, matching the project-level layout exactly.
58
+ * The historical on-disk path (`~/.fraim/personalized-employee/{stuff}`) is
59
+ * preserved via `contentRoot = join(root, 'personalized-employee')`.
60
+ *
61
+ * For org: `~/.fraim/org/` — capability content lives at
62
+ * `~/.fraim/org/personalized-employee/`. New installs land here automatically;
63
+ * existing installs with flat content at `~/.fraim/org/{stuff}` are migrated by
64
+ * `migrateOrgFlatContent`.
59
65
  */
60
66
  function standardHome(layer) {
61
67
  if (layer === 'manager')
62
- return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee');
68
+ return (0, project_fraim_paths_1.getUserFraimDirPath)();
63
69
  return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org');
64
70
  }
71
+ /**
72
+ * Auto-migrate flat org content (pre-#1070 layout: `~/.fraim/org/{stuff}`)
73
+ * to the uniform subdir layout (`~/.fraim/org/personalized-employee/{stuff}`).
74
+ * Runs at most once per process per orgRoot path (guarded by a Set).
75
+ * Never throws: migration failures are silently skipped.
76
+ */
77
+ const _migratedOrgRoots = new Set();
78
+ const ORG_CONTENT_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'employees'];
79
+ function migrateOrgFlatContent(orgRoot) {
80
+ if (_migratedOrgRoots.has(orgRoot))
81
+ return;
82
+ _migratedOrgRoots.add(orgRoot);
83
+ const dest = path_1.default.join(orgRoot, 'personalized-employee');
84
+ try {
85
+ for (const dir of ORG_CONTENT_DIRS) {
86
+ const src = path_1.default.join(orgRoot, dir);
87
+ if (!fs_1.default.existsSync(src))
88
+ continue;
89
+ // Skip if already under personalized-employee.
90
+ const destDir = path_1.default.join(dest, dir);
91
+ if (fs_1.default.existsSync(destDir))
92
+ continue;
93
+ fs_1.default.mkdirSync(dest, { recursive: true });
94
+ fs_1.default.renameSync(src, destDir);
95
+ }
96
+ }
97
+ catch { /* migration is best-effort; never fail a read over it */ }
98
+ }
65
99
  /**
66
100
  * Where the cloud backend materializes. Also the historical path for each
67
101
  * layer, so switching to or from `fraim-cloud` does not relocate anything.
@@ -81,57 +115,52 @@ function defaultCloneDir(layer) {
81
115
  function resolvePackHome(layer) {
82
116
  const config = (0, user_config_1.getLayerBackend)(layer);
83
117
  const standard = standardHome(layer);
118
+ const make = (root, extra) => {
119
+ if (layer === 'org')
120
+ migrateOrgFlatContent(root);
121
+ return { layer, root, contentRoot: path_1.default.join(root, 'personalized-employee'), ...extra };
122
+ };
84
123
  if (!config || config.backend === 'single-machine') {
85
- return {
86
- layer,
124
+ return make(standard, {
87
125
  backend: config?.backend ?? 'single-machine',
88
- root: standard,
89
126
  synced: false,
90
127
  requiresPublish: false,
91
- };
128
+ });
92
129
  }
93
130
  if (config.backend === 'local-folder' && config.localPath) {
94
- return {
95
- layer,
131
+ return make(absolutize(config.localPath), {
96
132
  backend: 'local-folder',
97
- root: absolutize(config.localPath),
98
133
  synced: true,
99
134
  // The sync client replicates the write; there is nothing further to do.
100
135
  requiresPublish: false,
101
136
  legacyRoot: standard,
102
- };
137
+ });
103
138
  }
104
139
  if (config.backend === 'git' && config.gitUrl) {
105
- return {
106
- layer,
140
+ return make(config.localPath ? absolutize(config.localPath) : defaultCloneDir(layer), {
107
141
  backend: 'git',
108
- root: config.localPath ? absolutize(config.localPath) : defaultCloneDir(layer),
109
142
  synced: true,
110
143
  requiresPublish: true,
111
144
  gitUrl: config.gitUrl,
112
145
  legacyRoot: standard,
113
- };
146
+ });
114
147
  }
115
148
  if (config.backend === 'fraim-cloud') {
116
- return {
117
- layer,
149
+ return make(cloudHome(layer), {
118
150
  backend: 'fraim-cloud',
119
- root: cloudHome(layer),
120
151
  synced: true,
121
152
  requiresPublish: true,
122
153
  legacyRoot: standard,
123
- };
154
+ });
124
155
  }
125
156
  // A backend named but missing its required pointer (git without gitUrl,
126
157
  // local-folder without localPath). Fall back to the standard home rather
127
158
  // than inventing a location.
128
- return {
129
- layer,
159
+ return make(standard, {
130
160
  backend: config.backend,
131
- root: standard,
132
161
  synced: false,
133
162
  requiresPublish: false,
134
- };
163
+ });
135
164
  }
136
165
  /**
137
166
  * Where to read this layer from. Exactly one directory: the home.
@@ -141,7 +170,7 @@ function resolvePackHome(layer) {
141
170
  * entry again, the single-place invariant has been lost.
142
171
  */
143
172
  function packReadRoots(layer) {
144
- return [resolvePackHome(layer).root];
173
+ return [resolvePackHome(layer).contentRoot];
145
174
  }
146
175
  /**
147
176
  * Content sitting at the standard local path that the configured home does not
@@ -155,9 +184,12 @@ function packReadRoots(layer) {
155
184
  */
156
185
  function findStrandedLegacyContent(layer) {
157
186
  const home = resolvePackHome(layer);
158
- if (!home.legacyRoot || home.legacyRoot === home.root)
187
+ // legacyRoot is the old standard home root; stranded content is content that
188
+ // exists at the old location but not in the new contentRoot.
189
+ const legacyContentRoot = home.legacyRoot ? path_1.default.join(home.legacyRoot, 'personalized-employee') : null;
190
+ if (!legacyContentRoot || legacyContentRoot === home.contentRoot)
159
191
  return [];
160
- if (!fs_1.default.existsSync(home.legacyRoot))
192
+ if (!fs_1.default.existsSync(legacyContentRoot))
161
193
  return [];
162
194
  const stranded = [];
163
195
  const walk = (absDir, relDir) => {
@@ -173,11 +205,11 @@ function findStrandedLegacyContent(layer) {
173
205
  const abs = path_1.default.join(absDir, entry.name);
174
206
  if (entry.isDirectory())
175
207
  walk(abs, rel);
176
- else if (entry.isFile() && !fs_1.default.existsSync(path_1.default.join(home.root, rel)))
208
+ else if (entry.isFile() && !fs_1.default.existsSync(path_1.default.join(home.contentRoot, rel)))
177
209
  stranded.push(rel);
178
210
  }
179
211
  };
180
- walk(home.legacyRoot, '');
212
+ walk(legacyContentRoot, '');
181
213
  return stranded.sort();
182
214
  }
183
215
  const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
@@ -65,16 +65,20 @@ function learningEntryHeadingRegex(flags = '') {
65
65
  function managerSecondaryLearningsBase() {
66
66
  const home = (0, pack_home_1.resolvePackHome)('manager');
67
67
  if (home.synced)
68
- return (0, path_1.join)(home.root, 'learnings');
68
+ return (0, path_1.join)(home.contentRoot, 'learnings');
69
69
  // pack-home-exempt: deliberate historical secondary location, see above.
70
70
  return (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', 'learnings');
71
71
  }
72
72
  function getLearningRoots(workspaceRoot) {
73
+ const managerHome = (0, pack_home_1.resolvePackHome)('manager');
74
+ const managerHomeBase = (0, path_1.join)(managerHome.contentRoot, 'learnings');
73
75
  return {
74
76
  globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
75
77
  globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
76
78
  managerCacheBase: managerSecondaryLearningsBase(),
77
79
  managerCacheDisplayBase: (0, project_fraim_paths_1.getUserFraimDisplayPath)('manager/learnings'),
80
+ managerHomeBase,
81
+ managerHomeDisplayBase: managerHomeBase.replace(/\\/g, '/'),
78
82
  repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
79
83
  };
80
84
  }
@@ -406,7 +410,7 @@ function resolveOrgLearningFile(repoLearningsBase, fileName) {
406
410
  if ((0, fs_1.existsSync)(repoPath)) {
407
411
  return { present: true, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
408
412
  }
409
- const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').root, 'learnings', fileName);
413
+ const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', fileName);
410
414
  if ((0, fs_1.existsSync)(cachePath)) {
411
415
  return { present: true, path: cachePath, displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`) };
412
416
  }
@@ -419,7 +423,7 @@ function resolveOrgLearningFile(repoLearningsBase, fileName) {
419
423
  */
420
424
  function resolveOrgLearningFileTiers(repoLearningsBase, fileName) {
421
425
  const tiers = [];
422
- const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').root, 'learnings', fileName);
426
+ const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', fileName);
423
427
  if ((0, fs_1.existsSync)(cachePath)) {
424
428
  tiers.push({
425
429
  level: 'org',
@@ -624,7 +628,7 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
624
628
  };
625
629
  }
626
630
  if (orgCacheEligible) {
627
- const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').root, relativePath);
631
+ const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, relativePath);
628
632
  if ((0, fs_1.existsSync)(cachePath)) {
629
633
  return {
630
634
  present: true,
@@ -633,7 +637,7 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
633
637
  }
634
638
  }
635
639
  if (!orgCacheEligible) {
636
- const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').root, relativePath);
640
+ const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
637
641
  if ((0, fs_1.existsSync)(managerCachePath)) {
638
642
  return {
639
643
  present: true,
@@ -795,7 +799,7 @@ function resolveTeamContextFile(workspaceRoot, key) {
795
799
  // destination (R5.1), so writePath stays empty and the caller must route
796
800
  // edits through the org backend's propose-and-approve flow.
797
801
  if (key === 'org' || key === 'orgRules') {
798
- const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').root, relativePath);
802
+ const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, relativePath);
799
803
  if ((0, fs_1.existsSync)(cachePath)) {
800
804
  return {
801
805
  present: true,
@@ -808,7 +812,7 @@ function resolveTeamContextFile(workspaceRoot, key) {
808
812
  }
809
813
  }
810
814
  if (key === 'manager' || key === 'managerRules') {
811
- const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').root, relativePath);
815
+ const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
812
816
  if ((0, fs_1.existsSync)(managerCachePath)) {
813
817
  return {
814
818
  present: true,
@@ -845,7 +849,7 @@ function resolveTeamContextFile(workspaceRoot, key) {
845
849
  function orgBrandCandidateDirs(workspaceRoot) {
846
850
  return [
847
851
  (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', 'context'),
848
- (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').root, 'context'),
852
+ (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'context'),
849
853
  (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'context'),
850
854
  ];
851
855
  }
@@ -1224,7 +1228,9 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
1224
1228
  }
1225
1229
  function levelDir(roots, level) {
1226
1230
  if (level === 'machine') {
1227
- return { dir: roots.globalPersonalBase, displayBase: roots.globalPersonalDisplayBase.replace(/\/$/, '') };
1231
+ // Issue #1070: use the configured manager home, not the portable authoring base.
1232
+ // For single-machine backends managerHomeBase == globalPersonalBase (no change).
1233
+ return { dir: roots.managerHomeBase, displayBase: roots.managerHomeDisplayBase.replace(/\/$/, '') };
1228
1234
  }
1229
1235
  return { dir: roots.repoLearningsBase, displayBase: REPO_LEARNINGS_REL };
1230
1236
  }
@@ -1268,7 +1274,8 @@ function resolveLearningFilePath(workspaceRoot, userId, ref) {
1268
1274
  if (ref.scope === 'org')
1269
1275
  return (0, path_1.join)(roots.repoLearningsBase, `org-${fileType}.md`);
1270
1276
  const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
1271
- const base = ref.level === 'project' ? roots.repoLearningsBase : roots.globalPersonalBase;
1277
+ // Issue #1070: for the 'machine' level use the configured manager home.
1278
+ const base = ref.level === 'project' ? roots.repoLearningsBase : roots.managerHomeBase;
1272
1279
  return (0, path_1.join)(base, `${resolvedUserId}-${fileType}.md`);
1273
1280
  }
1274
1281
  function titleCaseFromFileType(fileType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.256",
3
+ "version": "2.0.258",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "bin": {
6
6
  "fraim-hub": "bin/fraim-hub.js",
@@ -163,7 +163,7 @@
163
163
  "electron": "^41.2.2",
164
164
  "electron-updater": "^6.8.9",
165
165
  "express": "^5.2.1",
166
- "fraim": "2.0.256",
166
+ "fraim": "2.0.258",
167
167
  "mongodb": "^7.0.0",
168
168
  "node-cron": "4.2.1",
169
169
  "node-edge-tts": "^1.2.10",
@@ -15276,6 +15276,31 @@ function openInlineCreateEmployee(options) {
15276
15276
  roleField.appendChild(roleInput);
15277
15277
  body.appendChild(roleField);
15278
15278
 
15279
+ // Scope picker — Issue #1067: let the manager choose where the employee lives.
15280
+ const scopeField = document.createElement('div');
15281
+ scopeField.className = 'ipe-card-field';
15282
+ const scopeLabel = document.createElement('div');
15283
+ scopeLabel.className = 'ipe-card-label';
15284
+ scopeLabel.textContent = 'Visibility';
15285
+ const scopeSelect = document.createElement('select');
15286
+ scopeSelect.name = 'scope';
15287
+ scopeSelect.setAttribute('aria-label', 'Employee visibility');
15288
+ const scopeOptions = [
15289
+ { value: 'project', label: 'This project' },
15290
+ { value: 'manager', label: 'Yours (all projects)' },
15291
+ { value: 'org', label: 'Everyone' },
15292
+ ];
15293
+ for (const opt of scopeOptions) {
15294
+ const el = document.createElement('option');
15295
+ el.value = opt.value;
15296
+ el.textContent = opt.label;
15297
+ scopeSelect.appendChild(el);
15298
+ }
15299
+ scopeSelect.value = (isEdit && editEmployee.scope) ? editEmployee.scope : 'project';
15300
+ scopeField.appendChild(scopeLabel);
15301
+ scopeField.appendChild(scopeSelect);
15302
+ body.appendChild(scopeField);
15303
+
15279
15304
  // Job picker — searchable list matching the command palette style.
15280
15305
  const selectedJobIds = new Set(isEdit && Array.isArray(editEmployee.jobIds) ? editEmployee.jobIds : []);
15281
15306
  const jobsField = document.createElement('div');
@@ -15392,6 +15417,7 @@ function openInlineCreateEmployee(options) {
15392
15417
  role: roleInput.value.trim() || undefined,
15393
15418
  icon: { kind: 'generated', value: displayName },
15394
15419
  jobIds: pickedJobIds,
15420
+ scope: scopeSelect.value || 'project',
15395
15421
  };
15396
15422
  const resp = await fetch(isEdit
15397
15423
  ? '/api/ai-hub/custom-employees/' + encodeURIComponent(editEmployee.key)
@@ -15459,4 +15485,7 @@ if (typeof window !== 'undefined') {
15459
15485
  // Issue #866 R4: let the suite drive the same onboarding-settled refresh the
15460
15486
  // poll paths call, to assert the project view refreshes live without a restart.
15461
15487
  window.onProjectOnboardingSettled = onProjectOnboardingSettled;
15488
+ // Issue #1067: let the suite trigger the create-employee form so scope picker
15489
+ // tests can assert the rendered controls without depending on click coordinates.
15490
+ window.openInlineCreateEmployee = openInlineCreateEmployee;
15462
15491
  }