fraim-framework 2.0.184 → 2.0.185

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.
@@ -21,15 +21,6 @@ let isQuitting = false; // distinguishes window-close (→ tray) from app-quit
21
21
  // ---------------------------------------------------------------------------
22
22
  // Helpers
23
23
  // ---------------------------------------------------------------------------
24
- function tryCreateDbService() {
25
- try {
26
- const loaded = require('../fraim/db-service');
27
- return new loaded.FraimDbService();
28
- }
29
- catch {
30
- return undefined;
31
- }
32
- }
33
24
  function preferredWindowSize() {
34
25
  const { workAreaSize } = electron_1.screen.getPrimaryDisplay();
35
26
  return {
@@ -255,7 +246,8 @@ async function launchDesktopShell(options) {
255
246
  const certBundle = await (0, cert_store_1.loadOrCreateCert)();
256
247
  server = new server_1.AiHubServer({
257
248
  projectPath: options.projectPath,
258
- dbService: tryCreateDbService(),
249
+ // Issue #701: no local DB. Persona/manager-team state resolves through the hosted
250
+ // server via the Hub's default remote gateway (authenticated by the user's API key).
259
251
  httpsPort,
260
252
  certBundle,
261
253
  folderPicker: async () => {
@@ -0,0 +1,88 @@
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.HttpHubRemoteGateway = void 0;
7
+ exports.resolveFraimRemoteUrl = resolveFraimRemoteUrl;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ function resolveFraimRemoteUrl(explicit) {
10
+ return (explicit || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me').replace(/\/+$/, '');
11
+ }
12
+ /**
13
+ * Default HTTP-backed gateway. Mirrors the ProviderClient axios pattern:
14
+ * base URL from FRAIM_REMOTE_URL, `x-api-key` auth header, 10s timeout.
15
+ */
16
+ class HttpHubRemoteGateway {
17
+ constructor(serverUrl) {
18
+ this.baseURL = resolveFraimRemoteUrl(serverUrl);
19
+ }
20
+ client(apiKey) {
21
+ return axios_1.default.create({
22
+ baseURL: this.baseURL,
23
+ headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
24
+ timeout: 10000,
25
+ });
26
+ }
27
+ async getPersonaState(apiKey) {
28
+ if (!apiKey)
29
+ return null;
30
+ try {
31
+ const res = await this.client(apiKey).get('/api/personas/me');
32
+ return res.data;
33
+ }
34
+ catch (err) {
35
+ // 401 (no/expired key) or 404 (feature disabled) => treat as "no state" so the Hub
36
+ // renders a clean locked/not-signed-in view instead of crashing.
37
+ const status = err?.response?.status;
38
+ if (status === 401 || status === 404)
39
+ return null;
40
+ console.warn('[ai-hub] getPersonaState failed:', err?.message || err);
41
+ return null;
42
+ }
43
+ }
44
+ async listManagerTeam(apiKey) {
45
+ if (!apiKey)
46
+ return [];
47
+ try {
48
+ const res = await this.client(apiKey).get('/api/ai-hub/manager-team');
49
+ const team = (res.data?.team ?? []);
50
+ return Array.isArray(team) ? team : [];
51
+ }
52
+ catch (err) {
53
+ const status = err?.response?.status;
54
+ if (status === 401 || status === 404)
55
+ return [];
56
+ console.warn('[ai-hub] listManagerTeam failed:', err?.message || err);
57
+ return [];
58
+ }
59
+ }
60
+ async assignManagerTeam(apiKey, personaKey) {
61
+ if (!apiKey)
62
+ return { status: 401, body: { error: 'authentication_required' } };
63
+ try {
64
+ const res = await this.client(apiKey).post('/api/ai-hub/manager-team/assign', { personaKey });
65
+ return { status: res.status, body: res.data };
66
+ }
67
+ catch (err) {
68
+ if (err?.response)
69
+ return { status: err.response.status, body: err.response.data };
70
+ console.warn('[ai-hub] assignManagerTeam failed:', err?.message || err);
71
+ return { status: 500, body: { error: 'internal_error' } };
72
+ }
73
+ }
74
+ async removeManagerTeam(apiKey, personaKey) {
75
+ if (!apiKey)
76
+ return;
77
+ try {
78
+ await this.client(apiKey).delete(`/api/ai-hub/manager-team/assign/${encodeURIComponent(personaKey)}`);
79
+ }
80
+ catch (err) {
81
+ const status = err?.response?.status;
82
+ if (status === 401 || status === 404)
83
+ return;
84
+ console.warn('[ai-hub] removeManagerTeam failed:', err?.message || err);
85
+ }
86
+ }
87
+ }
88
+ exports.HttpHubRemoteGateway = HttpHubRemoteGateway;
@@ -56,6 +56,7 @@ const hosts_1 = require("./hosts");
56
56
  const manager_turns_1 = require("./manager-turns");
57
57
  const preferences_1 = require("./preferences");
58
58
  const conversation_store_1 = require("./conversation-store");
59
+ const remote_hub_gateway_1 = require("./remote-hub-gateway");
59
60
  const managed_browser_1 = require("./managed-browser");
60
61
  const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
61
62
  let personaHiringModule;
@@ -137,29 +138,6 @@ function buildHubManagerHiringCatalog() {
137
138
  roles: {},
138
139
  };
139
140
  }
140
- async function getHubWorkspacePersonaState(dbService, userId, apiKey) {
141
- try {
142
- // eslint-disable-next-line @typescript-eslint/no-var-requires
143
- const service = require('../services/persona-entitlement-service');
144
- return await service.getWorkspacePersonaState(dbService, userId, apiKey);
145
- }
146
- catch {
147
- return null;
148
- }
149
- }
150
- function createDefaultDbService() {
151
- try {
152
- // eslint-disable-next-line @typescript-eslint/no-var-requires
153
- const { FraimDbService } = require('../fraim/db-service');
154
- return new FraimDbService();
155
- }
156
- catch {
157
- return undefined;
158
- }
159
- }
160
- function supportsManagerSeatAssignments(dbService) {
161
- return typeof dbService.countHubManagerAssignments === 'function';
162
- }
163
141
  class AiHubRunRegistry {
164
142
  constructor() {
165
143
  this.runs = new Map();
@@ -398,19 +376,22 @@ function normalizeReviewHandoff(raw) {
398
376
  const url = safeHttpUrl(rawTarget?.url);
399
377
  if (!url)
400
378
  return null;
379
+ if (artifacts.length > 0)
380
+ return null;
401
381
  return {
402
382
  reviewRequired: true,
403
383
  reviewTarget: { type: 'pull_request', label: targetLabel || 'Pull request', url },
404
- artifacts,
384
+ artifacts: [],
405
385
  summary: typeof value.summary === 'string' ? value.summary.trim() : '',
406
386
  feedbackMode: typeof value.feedbackMode === 'string' ? value.feedbackMode.trim() : 'pull_request_comments',
407
387
  };
408
388
  }
409
- if (targetType === 'artifact_set' && artifacts.length > 0) {
389
+ const fileArtifacts = artifacts.filter((artifact) => artifact.path && !artifact.url);
390
+ if (targetType === 'artifact_set' && fileArtifacts.length > 0 && fileArtifacts.length === artifacts.length) {
410
391
  return {
411
392
  reviewRequired: true,
412
393
  reviewTarget: { type: 'artifact_set', label: targetLabel || `${artifacts.length} artifact${artifacts.length === 1 ? '' : 's'}` },
413
- artifacts,
394
+ artifacts: fileArtifacts,
414
395
  summary: typeof value.summary === 'string' ? value.summary.trim() : '',
415
396
  feedbackMode: typeof value.feedbackMode === 'string' ? value.feedbackMode.trim() : 'inline',
416
397
  };
@@ -573,31 +554,6 @@ function extractDelegationLedgerFromText(text) {
573
554
  }
574
555
  return null;
575
556
  }
576
- const ARTIFACT_EXCLUDE_RE = /(^|[\\/])(retrospectives|evidence|learnings|mocks|raw|archive)[\\/]/i;
577
- function extractReviewArtifactsFromText(text, projectPath) {
578
- if (!text)
579
- return [];
580
- const artifacts = [];
581
- const seen = new Set();
582
- const matches = String(text)
583
- .split(/[\s`"'()<>{}\[\],;:]+/)
584
- .filter((token) => /^(docs|public|src|tests)[\\/]/.test(token));
585
- for (const candidate of matches) {
586
- const relativePath = candidate.replace(/[.)]+$/g, '').replace(/\\/g, '/');
587
- if (!/\.[A-Za-z0-9]+$/.test(relativePath))
588
- continue;
589
- if (ARTIFACT_EXCLUDE_RE.test(relativePath))
590
- continue;
591
- const parts = relativePath.split('/').filter(Boolean);
592
- const name = parts[parts.length - 1] || relativePath;
593
- const artifactPath = path_1.default.isAbsolute(relativePath) ? relativePath : path_1.default.join(projectPath, relativePath);
594
- if (seen.has(artifactPath))
595
- continue;
596
- seen.add(artifactPath);
597
- artifacts.push({ type: path_1.default.extname(name).replace(/^\./, '') || 'file', label: name, path: artifactPath });
598
- }
599
- return artifacts;
600
- }
601
557
  function applyReviewProjection(run, text) {
602
558
  const delegation = extractDelegationLedgerFromText(text);
603
559
  if (delegation) {
@@ -610,41 +566,9 @@ function applyReviewProjection(run, text) {
610
566
  const handoff = extractReviewHandoffFromText(text);
611
567
  if (handoff) {
612
568
  run.reviewHandoff = handoff;
613
- run.artifacts = handoff.artifacts;
569
+ run.artifacts = handoff.reviewTarget?.type === 'artifact_set' ? handoff.artifacts : [];
614
570
  return;
615
571
  }
616
- const artifacts = extractReviewArtifactsFromText(text, run.projectPath);
617
- if (!artifacts.length)
618
- return;
619
- run.artifacts = run.artifacts || [];
620
- for (const artifact of artifacts) {
621
- if (!run.artifacts.some((entry) => (entry.path && artifact.path && entry.path === artifact.path) || (entry.url && artifact.url && entry.url === artifact.url))) {
622
- run.artifacts.push(artifact);
623
- }
624
- }
625
- }
626
- function mergeReviewArtifacts(existing, discovered) {
627
- const merged = [...(existing || [])];
628
- for (const artifact of discovered) {
629
- if (!merged.some((entry) => (entry.path && artifact.path && entry.path === artifact.path) || (entry.url && artifact.url && entry.url === artifact.url))) {
630
- merged.push(artifact);
631
- }
632
- }
633
- return merged;
634
- }
635
- function extractRunReviewArtifacts(run) {
636
- const discovered = [];
637
- for (const message of run.messages || []) {
638
- if (message.role !== 'employee')
639
- continue;
640
- discovered.push(...extractReviewArtifactsFromText(message.text, run.projectPath));
641
- }
642
- for (const event of run.events || []) {
643
- if (event.channel !== 'stdout')
644
- continue;
645
- discovered.push(...extractReviewArtifactsFromText(event.text, run.projectPath));
646
- }
647
- return mergeReviewArtifacts(run.artifacts, discovered);
648
572
  }
649
573
  function emptyTotals() {
650
574
  return {
@@ -750,8 +674,13 @@ function applySeekMentoringSignal(run, signal) {
750
674
  const callJobId = signal.jobId || signal.jobName;
751
675
  if (callJobId && targetJobId && callJobId !== targetJobId)
752
676
  return;
753
- if (signal.reviewHandoff)
754
- run.reviewHandoff = signal.reviewHandoff;
677
+ if (signal.reviewHandoff) {
678
+ const normalizedReviewHandoff = normalizeReviewHandoff(signal.reviewHandoff);
679
+ run.reviewHandoff = normalizedReviewHandoff || signal.reviewHandoff;
680
+ run.artifacts = normalizedReviewHandoff?.reviewTarget?.type === 'artifact_set'
681
+ ? normalizedReviewHandoff.artifacts
682
+ : [];
683
+ }
755
684
  if (signal.delegationLedger &&
756
685
  run.jobId === 'fully-delegate' &&
757
686
  signal.phaseStatus === 'complete' &&
@@ -1154,14 +1083,14 @@ class AiHubServer {
1154
1083
  explicitPath: process.env.FRAIM_BROWSER_PATH || undefined,
1155
1084
  });
1156
1085
  this.hostRuntime = options.hostRuntime || (process.env.FRAIM_AI_HUB_FAKE_HOST === '1' ? new hosts_1.FakeHostRuntime() : new hosts_1.CliHostRuntime());
1157
- if (options.dbService !== undefined) {
1158
- this.dbService = options.dbService;
1159
- this.ownsDbService = false;
1160
- }
1161
- else {
1162
- this.dbService = createDefaultDbService();
1163
- this.ownsDbService = this.dbService !== undefined;
1164
- }
1086
+ // Issue #701: the AI Hub is a loopback companion that never touches MongoDB directly —
1087
+ // on every machine (dev, CI, prod) it resolves persona/manager-team state from the hosted
1088
+ // server through the same code path. The hosted server is the sole owner of DB access.
1089
+ // A dbService is used ONLY when explicitly injected (e.g. the hosted server embedding the
1090
+ // Hub, or a test); it is never auto-created, so there is no dev/prod divergence.
1091
+ this.dbService = options.dbService;
1092
+ this.ownsDbService = false;
1093
+ this.remoteGateway = options.remoteGateway ?? new remote_hub_gateway_1.HttpHubRemoteGateway();
1165
1094
  this.deploymentStore = options.deploymentStore ?? new DeploymentStore();
1166
1095
  this.hostConfigStore = options.hostConfigStore ?? new HostConfigStore();
1167
1096
  this.app.use(express_1.default.json({ limit: '10mb' }));
@@ -1429,8 +1358,9 @@ class AiHubServer {
1429
1358
  requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
1430
1359
  }));
1431
1360
  const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath, catalogOptions);
1432
- const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(apiKey || preferences.apiKey);
1433
- const managerTeam = await this.computeManagerTeam(workspaceId, userKey);
1361
+ const resolvedApiKey = apiKey || preferences.apiKey;
1362
+ const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(resolvedApiKey);
1363
+ const managerTeam = await this.computeManagerTeam(resolvedApiKey);
1434
1364
  const projects = this.knownProjects(normalizedProjectPath);
1435
1365
  preferences = { ...preferences, projectPath: normalizedProjectPath, projects };
1436
1366
  this.preferencesStore.save(preferences);
@@ -1906,18 +1836,18 @@ class AiHubServer {
1906
1836
  seatCount: 0,
1907
1837
  seatsInUse: 0,
1908
1838
  }));
1909
- if (!this.dbService) {
1910
- return { personas: fallbackPersonas, subscriptionActive: false, workspaceId: null, userKey: null };
1911
- }
1912
1839
  try {
1913
- const state = await getHubWorkspacePersonaState(this.dbService, getHubUserEmail(), apiKey ?? '');
1840
+ // Issue #701: persona state comes from the hosted server (GET /api/personas/me)
1841
+ // via the user's API key — never a local MongoDB connection. A null result means
1842
+ // no/expired key or the feature is off; render the locked fallback (not-signed-in).
1843
+ const state = await this.remoteGateway.getPersonaState(apiKey);
1914
1844
  if (!state) {
1915
1845
  return { personas: fallbackPersonas, subscriptionActive: false, workspaceId: null, userKey: null };
1916
1846
  }
1917
1847
  // Legacy bypass: persona gating is not active for this workspace, so all personas are accessible.
1918
1848
  // Mirrors the evaluatePersonaAccess legacy bypass in persona-entitlement-service.ts.
1849
+ // Seats are not assignable on legacy workspaces (the hosted server is always DB-backed).
1919
1850
  if (!state.subscriptionActive) {
1920
- const legacySeatCount = supportsManagerSeatAssignments(this.dbService) ? 0 : 1;
1921
1851
  const allPersonas = allBundles.map((bundle) => ({
1922
1852
  key: bundle.personaKey,
1923
1853
  displayName: bundle.catalogMetadata.displayName,
@@ -1926,7 +1856,7 @@ class AiHubServer {
1926
1856
  pricingLabel: '',
1927
1857
  status: 'hired',
1928
1858
  hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
1929
- seatCount: legacySeatCount,
1859
+ seatCount: 0,
1930
1860
  seatsInUse: 0,
1931
1861
  }));
1932
1862
  return { personas: allPersonas, subscriptionActive: false, workspaceId: state.workspaceId, userKey: state.userId ?? null };
@@ -1942,13 +1872,13 @@ class AiHubServer {
1942
1872
  seatCountByKey[e.personaKey] = (seatCountByKey[e.personaKey] ?? 0) + (e.jobCreditsRemaining ?? 1);
1943
1873
  }
1944
1874
  }
1945
- // seatsInUse is the count of manager-assignment rows for this workspace/persona.
1946
- const workspaceId = state.workspaceId;
1875
+ // seatsInUse for display: derived from the hosted manager team. Authoritative
1876
+ // out-of-stock enforcement lives on the hosted assign endpoint (server-side),
1877
+ // so this display count does not need to be workspace-wide.
1947
1878
  const seatsInUseByKey = {};
1948
- if (this.dbService) {
1949
- await Promise.all(Object.keys(seatCountByKey).map(async (pKey) => {
1950
- seatsInUseByKey[pKey] = await this.dbService.countHubManagerAssignments(workspaceId, pKey);
1951
- }));
1879
+ const team = await this.remoteGateway.listManagerTeam(apiKey);
1880
+ for (const entry of team) {
1881
+ seatsInUseByKey[entry.personaKey] = (seatsInUseByKey[entry.personaKey] ?? 0) + 1;
1952
1882
  }
1953
1883
  const personas = allBundles.map((bundle) => ({
1954
1884
  key: bundle.personaKey,
@@ -1961,23 +1891,16 @@ class AiHubServer {
1961
1891
  seatCount: seatCountByKey[bundle.personaKey] ?? 0,
1962
1892
  seatsInUse: seatsInUseByKey[bundle.personaKey] ?? 0,
1963
1893
  }));
1964
- return { personas, subscriptionActive: state.subscriptionActive, workspaceId, userKey: state.userId ?? null };
1894
+ return { personas, subscriptionActive: state.subscriptionActive, workspaceId: state.workspaceId, userKey: state.userId ?? null };
1965
1895
  }
1966
1896
  catch (err) {
1967
- console.error('[ai-hub] persona entitlement lookup failed:', err);
1897
+ console.error('[ai-hub] persona lookup failed:', err);
1968
1898
  return { personas: fallbackPersonas, subscriptionActive: false, workspaceId: null, userKey: null };
1969
1899
  }
1970
1900
  }
1971
- async computeManagerTeam(workspaceId, userKey) {
1972
- if (!workspaceId || !userKey || !this.dbService)
1973
- return [];
1974
- try {
1975
- const rows = await this.dbService.getHubManagerAssignments(workspaceId, userKey);
1976
- return rows.map((r) => ({ personaKey: r.personaKey, assignedAt: r.assignedAt.toISOString() }));
1977
- }
1978
- catch {
1979
- return [];
1980
- }
1901
+ async computeManagerTeam(apiKey) {
1902
+ // Issue #701: manager team comes from the hosted server, not local Mongo.
1903
+ return this.remoteGateway.listManagerTeam(apiKey);
1981
1904
  }
1982
1905
  registerRoutes() {
1983
1906
  // Issue #512: Serve the account and analytics pages from public/.
@@ -2121,57 +2044,25 @@ class AiHubServer {
2121
2044
  return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not write learning.' });
2122
2045
  }
2123
2046
  });
2124
- // Issue #540: POST /api/ai-hub/manager-team/assign
2125
- // Assigns a persona to the authenticated manager's team (uses X-Fraim-Api-Key header).
2126
- // Returns 404 {error:'no_company_seat'} when the workspace has purchased no seats for the persona.
2127
- // Returns 409 {error:'out_of_stock'} when all company seats are already assigned to other managers.
2047
+ // Issue #540/#701: POST /api/ai-hub/manager-team/assign
2048
+ // Proxies to the hosted server (which owns the DB + seat enforcement). The Hub
2049
+ // accepts the local X-Fraim-Api-Key header and forwards it as the hosted x-api-key.
2050
+ // Hosted returns 404 no_company_seat / 409 out_of_stock; those are passed through.
2128
2051
  this.app.post('/api/ai-hub/manager-team/assign', async (req, res) => {
2129
2052
  const apiKey = typeof req.headers['x-fraim-api-key'] === 'string' ? req.headers['x-fraim-api-key'] : undefined;
2130
2053
  const { personaKey } = (req.body ?? {});
2131
2054
  if (!personaKey)
2132
2055
  return res.status(400).json({ error: 'personaKey required' });
2133
- if (!this.dbService) {
2134
- return res.status(404).json({ error: 'no_company_seat', personaKey });
2135
- }
2136
- try {
2137
- const { workspaceId, userKey, personas } = await this.computePersonas(apiKey);
2138
- if (!workspaceId || !userKey) {
2139
- return res.status(404).json({ error: 'no_company_seat', personaKey });
2140
- }
2141
- const persona = personas.find((p) => p.key === personaKey);
2142
- if (!persona || persona.seatCount === 0) {
2143
- return res.status(404).json({ error: 'no_company_seat', personaKey });
2144
- }
2145
- if (persona.seatsInUse >= persona.seatCount) {
2146
- return res.status(409).json({ error: 'out_of_stock', personaKey, seatCount: persona.seatCount });
2147
- }
2148
- await this.dbService.addHubManagerAssignment(workspaceId, userKey, personaKey);
2149
- const team = await this.computeManagerTeam(workspaceId, userKey);
2150
- return res.json({ managerTeam: team });
2151
- }
2152
- catch (err) {
2153
- console.error('[ai-hub] assign manager seat failed:', err);
2154
- return res.status(500).json({ error: 'internal_error' });
2155
- }
2056
+ const result = await this.remoteGateway.assignManagerTeam(apiKey, personaKey);
2057
+ return res.status(result.status).json(result.body);
2156
2058
  });
2157
- // Issue #540: DELETE /api/ai-hub/manager-team/assign/:personaKey
2158
- // Removes a persona from the authenticated manager's team.
2059
+ // Issue #540/#701: DELETE /api/ai-hub/manager-team/assign/:personaKey
2060
+ // Proxies the seat release to the hosted server.
2159
2061
  this.app.delete('/api/ai-hub/manager-team/assign/:personaKey', async (req, res) => {
2160
2062
  const apiKey = typeof req.headers['x-fraim-api-key'] === 'string' ? req.headers['x-fraim-api-key'] : undefined;
2161
2063
  const { personaKey } = req.params;
2162
- if (!this.dbService)
2163
- return res.status(204).end();
2164
- try {
2165
- const { workspaceId, userKey } = await this.computePersonas(apiKey);
2166
- if (workspaceId && userKey) {
2167
- await this.dbService.removeHubManagerAssignment(workspaceId, userKey, personaKey);
2168
- }
2169
- return res.status(204).end();
2170
- }
2171
- catch (err) {
2172
- console.error('[ai-hub] remove manager seat failed:', err);
2173
- return res.status(500).json({ error: 'internal_error' });
2174
- }
2064
+ await this.remoteGateway.removeManagerTeam(apiKey, personaKey);
2065
+ return res.status(204).end();
2175
2066
  });
2176
2067
  this.app.get('/api/ai-hub/conversations', (req, res) => {
2177
2068
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
@@ -3435,7 +3326,7 @@ class AiHubServer {
3435
3326
  liveTotals.waitingDurationMs = Math.max(0, liveTotals.waitingDurationMs - overflow);
3436
3327
  }
3437
3328
  }
3438
- return { ...run, stages, totals: liveTotals, artifacts: extractRunReviewArtifacts(run) };
3329
+ return { ...run, stages, totals: liveTotals, artifacts: run.artifacts || [] };
3439
3330
  }
3440
3331
  }
3441
3332
  exports.AiHubServer = AiHubServer;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getManagerTeamFor = getManagerTeamFor;
4
+ exports.assignManagerSeatFor = assignManagerSeatFor;
5
+ exports.removeManagerSeatFor = removeManagerSeatFor;
6
+ exports.listManagerTeam = listManagerTeam;
7
+ exports.assignManagerTeam = assignManagerTeam;
8
+ exports.removeManagerTeam = removeManagerTeam;
9
+ const persona_entitlement_service_1 = require("../../services/persona-entitlement-service");
10
+ async function loadTeam(dbService, workspaceId, userKey) {
11
+ const rows = await dbService.getHubManagerAssignments(workspaceId, userKey);
12
+ return rows.map((r) => ({ personaKey: r.personaKey, assignedAt: r.assignedAt.toISOString() }));
13
+ }
14
+ /** Seats purchased for a persona in this workspace. Mirrors computePersonas' seatCountByKey:
15
+ * legacy (unsubscribed) workspaces have no assignable seats. */
16
+ function seatCountForPersona(state, personaKey) {
17
+ if (!state.subscriptionActive)
18
+ return 0;
19
+ return state.entitlements
20
+ .filter((e) => e.personaKey === personaKey && e.status === 'active')
21
+ .reduce((sum, e) => sum + (e.jobCreditsRemaining ?? 1), 0);
22
+ }
23
+ // ── Core operations (shared by hosted handlers and the Hub's local-DB gateway) ──────
24
+ async function getManagerTeamFor(dbService, userId, apiKey) {
25
+ const state = await (0, persona_entitlement_service_1.getWorkspacePersonaState)(dbService, userId, apiKey);
26
+ return loadTeam(dbService, state.workspaceId, state.userId);
27
+ }
28
+ async function assignManagerSeatFor(dbService, userId, apiKey, personaKey) {
29
+ const state = await (0, persona_entitlement_service_1.getWorkspacePersonaState)(dbService, userId, apiKey);
30
+ const seatCount = seatCountForPersona(state, personaKey);
31
+ if (seatCount === 0)
32
+ return { status: 404, body: { error: 'no_company_seat', personaKey } };
33
+ const seatsInUse = await dbService.countHubManagerAssignments(state.workspaceId, personaKey);
34
+ if (seatsInUse >= seatCount)
35
+ return { status: 409, body: { error: 'out_of_stock', personaKey, seatCount } };
36
+ await dbService.addHubManagerAssignment(state.workspaceId, state.userId, personaKey);
37
+ return { status: 200, body: { managerTeam: await loadTeam(dbService, state.workspaceId, state.userId) } };
38
+ }
39
+ async function removeManagerSeatFor(dbService, userId, apiKey, personaKey) {
40
+ const state = await (0, persona_entitlement_service_1.getWorkspacePersonaState)(dbService, userId, apiKey);
41
+ await dbService.removeHubManagerAssignment(state.workspaceId, state.userId, personaKey);
42
+ }
43
+ // ── Hosted HTTP handlers (thin wrappers over the core, authed via req.apiKeyData) ───
44
+ async function listManagerTeam(req, res, dbService) {
45
+ const apiKeyData = req.apiKeyData;
46
+ if (!apiKeyData?.userId) {
47
+ res.status(401).json({ error: 'Authentication required' });
48
+ return;
49
+ }
50
+ try {
51
+ res.json({ team: await getManagerTeamFor(dbService, apiKeyData.userId, apiKeyData.key) });
52
+ }
53
+ catch (error) {
54
+ console.error('[api] listManagerTeam failed:', error);
55
+ res.status(500).json({ error: 'internal_error', details: error?.message || String(error) });
56
+ }
57
+ }
58
+ async function assignManagerTeam(req, res, dbService) {
59
+ const apiKeyData = req.apiKeyData;
60
+ if (!apiKeyData?.userId) {
61
+ res.status(401).json({ error: 'Authentication required' });
62
+ return;
63
+ }
64
+ const { personaKey } = (req.body ?? {});
65
+ if (!personaKey) {
66
+ res.status(400).json({ error: 'personaKey required' });
67
+ return;
68
+ }
69
+ try {
70
+ const result = await assignManagerSeatFor(dbService, apiKeyData.userId, apiKeyData.key, personaKey);
71
+ res.status(result.status).json(result.body);
72
+ }
73
+ catch (error) {
74
+ console.error('[api] assignManagerTeam failed:', error);
75
+ res.status(500).json({ error: 'internal_error', details: error?.message || String(error) });
76
+ }
77
+ }
78
+ async function removeManagerTeam(req, res, dbService) {
79
+ const apiKeyData = req.apiKeyData;
80
+ if (!apiKeyData?.userId) {
81
+ res.status(401).json({ error: 'Authentication required' });
82
+ return;
83
+ }
84
+ const personaKey = String(req.params.personaKey ?? '');
85
+ try {
86
+ await removeManagerSeatFor(dbService, apiKeyData.userId, apiKeyData.key, personaKey);
87
+ res.status(204).end();
88
+ }
89
+ catch (error) {
90
+ console.error('[api] removeManagerTeam failed:', error);
91
+ res.status(500).json({ error: 'internal_error', details: error?.message || String(error) });
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.184",
3
+ "version": "2.0.185",
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": {