fraim-framework 2.0.167 → 2.0.169
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/dist/src/ai-hub/catalog.js +28 -14
- package/dist/src/ai-hub/server.js +34 -406
- package/dist/src/cli/commands/init-project.js +1 -98
- package/dist/src/cli/commands/manager.js +40 -0
- package/dist/src/cli/commands/sync.js +17 -21
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/utils/github-workflow-sync.js +12 -146
- package/dist/src/cli/utils/manager-pack-sync.js +188 -0
- package/dist/src/cli/utils/manager-publish.js +76 -0
- package/dist/src/cli/utils/user-config.js +20 -0
- package/dist/src/core/fraim-config-schema.generated.js +85 -10
- package/dist/src/core/manager-pack.js +26 -0
- package/dist/src/first-run/install-state.js +1 -0
- package/dist/src/first-run/server.js +9 -0
- package/dist/src/first-run/session-service.js +117 -23
- package/dist/src/first-run/types.js +2 -5
- package/dist/src/local-mcp-server/learning-context-builder.js +45 -8
- package/dist/src/local-mcp-server/stdio-server.js +28 -0
- package/package.json +1 -2
- package/public/ai-hub/index.html +0 -81
- package/public/ai-hub/script.js +3 -219
- package/public/ai-hub/styles.css +8 -36
- package/public/first-run/index.html +1 -1
- package/public/first-run/script.js +459 -530
- package/public/first-run/styles.css +288 -73
- package/dist/src/config/ai-manager-hiring.js +0 -121
- package/dist/src/config/compat.js +0 -16
- package/dist/src/config/feature-flags.js +0 -25
- package/dist/src/config/persona-capability-bundles.js +0 -273
- package/dist/src/config/persona-hiring.js +0 -270
- package/dist/src/config/portfolio-slug-overrides.js +0 -17
- package/dist/src/config/pricing.js +0 -37
- package/dist/src/config/stripe.js +0 -43
|
@@ -16,11 +16,11 @@ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
|
16
16
|
// Directories scanned for employee jobs at runtime, in lowest-to-highest
|
|
17
17
|
// precedence order. Later entries win on {categoryId, jobId} collision.
|
|
18
18
|
//
|
|
19
|
-
// FRAIM execution
|
|
20
|
-
// and the personalized override.
|
|
21
|
-
//
|
|
22
|
-
// - <project>/fraim/ai-employee/jobs/<category>/
|
|
23
|
-
// - <project>/fraim/personalized-employee/jobs/<category>/
|
|
19
|
+
// FRAIM execution normally reads only the two `fraim/` layers: the synced
|
|
20
|
+
// baseline and the personalized override. The Hub can explicitly opt into the
|
|
21
|
+
// source `registry/` fallback when running against the FRAIM repo before sync.
|
|
22
|
+
// - <project>/fraim/ai-employee/jobs/<category>/ - synced baseline
|
|
23
|
+
// - <project>/fraim/personalized-employee/jobs/<category>/ - taught/customized override
|
|
24
24
|
// Only the personalized-employee layer is "personalized" (issue #566).
|
|
25
25
|
const EMPLOYEE_JOB_LAYERS = [
|
|
26
26
|
{ segments: ['ai-employee', 'jobs'], personalized: false },
|
|
@@ -31,6 +31,12 @@ const MANAGER_JOB_LAYERS = [
|
|
|
31
31
|
{ segments: ['ai-manager', 'jobs'], personalized: false },
|
|
32
32
|
{ segments: ['personalized-employee', 'manager-jobs'], personalized: true },
|
|
33
33
|
];
|
|
34
|
+
const REGISTRY_EMPLOYEE_JOB_LAYERS = [
|
|
35
|
+
{ base: 'project', segments: ['registry', 'jobs', 'ai-employee'], personalized: false },
|
|
36
|
+
];
|
|
37
|
+
const REGISTRY_MANAGER_JOB_LAYERS = [
|
|
38
|
+
{ base: 'project', segments: ['registry', 'jobs', 'ai-manager'], personalized: false },
|
|
39
|
+
];
|
|
34
40
|
const KNOWN_LABEL_OVERRIDES = {
|
|
35
41
|
// Project conventions where a directory name should render as a recognized
|
|
36
42
|
// short form rather than its mechanical title-case spelling.
|
|
@@ -151,6 +157,8 @@ function summarizeProject(projectPath) {
|
|
|
151
157
|
};
|
|
152
158
|
}
|
|
153
159
|
function resolveLayerRoot(projectPath, layer) {
|
|
160
|
+
if (layer.base === 'project')
|
|
161
|
+
return path_1.default.join(projectPath, ...layer.segments);
|
|
154
162
|
return path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath), ...layer.segments);
|
|
155
163
|
}
|
|
156
164
|
function discoverLayers(projectPath, layers) {
|
|
@@ -168,11 +176,13 @@ function discoverLayers(projectPath, layers) {
|
|
|
168
176
|
}
|
|
169
177
|
return out;
|
|
170
178
|
}
|
|
171
|
-
function discoverEmployeeJobs(projectPath) {
|
|
179
|
+
function discoverEmployeeJobs(projectPath, options = {}) {
|
|
172
180
|
const project = summarizeProject(projectPath);
|
|
173
|
-
if (!project.exists || !project.hasFraim)
|
|
181
|
+
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
174
182
|
return [];
|
|
175
|
-
const layers = discoverLayers(projectPath,
|
|
183
|
+
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
184
|
+
? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
|
|
185
|
+
: EMPLOYEE_JOB_LAYERS);
|
|
176
186
|
// Group by categoryId so all layers contribute to the same labelled category.
|
|
177
187
|
const jobsByKey = new Map();
|
|
178
188
|
for (const layer of layers) {
|
|
@@ -193,11 +203,13 @@ function discoverEmployeeJobs(projectPath) {
|
|
|
193
203
|
return a.title.localeCompare(b.title);
|
|
194
204
|
});
|
|
195
205
|
}
|
|
196
|
-
function discoverManagerTemplates(projectPath) {
|
|
206
|
+
function discoverManagerTemplates(projectPath, options = {}) {
|
|
197
207
|
const project = summarizeProject(projectPath);
|
|
198
|
-
if (!project.exists || !project.hasFraim)
|
|
208
|
+
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
199
209
|
return [];
|
|
200
|
-
const layers = discoverLayers(projectPath,
|
|
210
|
+
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
211
|
+
? [...REGISTRY_MANAGER_JOB_LAYERS, ...MANAGER_JOB_LAYERS]
|
|
212
|
+
: MANAGER_JOB_LAYERS);
|
|
201
213
|
const templatesByKey = new Map();
|
|
202
214
|
for (const layer of layers) {
|
|
203
215
|
const groupLabel = humanizeName(layer.categoryId);
|
|
@@ -372,12 +384,14 @@ function labelForPhaseId(phaseId, jobId, projectPath) {
|
|
|
372
384
|
}
|
|
373
385
|
return friendlyPhaseLabel(phaseId);
|
|
374
386
|
}
|
|
375
|
-
function getAiHubCategories(projectPath) {
|
|
387
|
+
function getAiHubCategories(projectPath, options = {}) {
|
|
376
388
|
const project = summarizeProject(projectPath);
|
|
377
|
-
if (!project.exists || !project.hasFraim)
|
|
389
|
+
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
378
390
|
return [];
|
|
379
391
|
// A category is any directory found at any layer; deduplicate by id.
|
|
380
|
-
const layers = discoverLayers(projectPath,
|
|
392
|
+
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
393
|
+
? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
|
|
394
|
+
: EMPLOYEE_JOB_LAYERS);
|
|
381
395
|
const seen = new Map();
|
|
382
396
|
for (const layer of layers) {
|
|
383
397
|
if (!seen.has(layer.categoryId)) {
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.AiHubServer =
|
|
39
|
+
exports.AiHubServer = void 0;
|
|
40
40
|
exports.findAvailablePort = findAvailablePort;
|
|
41
41
|
exports.findAvailablePortExcluding = findAvailablePortExcluding;
|
|
42
42
|
const express_1 = __importDefault(require("express"));
|
|
@@ -50,7 +50,6 @@ const https_1 = __importDefault(require("https"));
|
|
|
50
50
|
const types_1 = require("../first-run/types");
|
|
51
51
|
const learning_context_builder_1 = require("../local-mcp-server/learning-context-builder");
|
|
52
52
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
53
|
-
const persona_hiring_1 = require("../config/persona-hiring");
|
|
54
53
|
const catalog_1 = require("./catalog");
|
|
55
54
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
56
55
|
const hosts_1 = require("./hosts");
|
|
@@ -59,6 +58,25 @@ const preferences_1 = require("./preferences");
|
|
|
59
58
|
const conversation_store_1 = require("./conversation-store");
|
|
60
59
|
const managed_browser_1 = require("./managed-browser");
|
|
61
60
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
61
|
+
let personaHiringModule;
|
|
62
|
+
function loadPersonaHiringModule() {
|
|
63
|
+
const cached = personaHiringModule;
|
|
64
|
+
if (cached !== undefined) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
// Server deployments include the persona catalog. The npm client package
|
|
69
|
+
// intentionally does not, so importing ai-hub/server must not require it.
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
71
|
+
const loaded = require('../config/persona-hiring');
|
|
72
|
+
personaHiringModule = loaded;
|
|
73
|
+
return loaded;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
personaHiringModule = null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
function loadPersonaCapabilityModule() {
|
|
63
81
|
try {
|
|
64
82
|
// Server deployments include the persona catalog. The npm client package
|
|
@@ -80,6 +98,9 @@ function buildHubPersonaHireUrl(personaKey, hireMode = 'job') {
|
|
|
80
98
|
const params = new URLSearchParams({ persona: personaKey, mode: hireMode });
|
|
81
99
|
return `/pricing?${params.toString()}`;
|
|
82
100
|
}
|
|
101
|
+
function buildHubPersonaAvatarUrl(personaKey) {
|
|
102
|
+
return loadPersonaHiringModule()?.buildPersonaAvatarUrl(personaKey) ?? '';
|
|
103
|
+
}
|
|
83
104
|
async function getHubWorkspacePersonaState(dbService, userId, apiKey) {
|
|
84
105
|
try {
|
|
85
106
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -180,113 +201,6 @@ function safeHttpUrl(value) {
|
|
|
180
201
|
return null;
|
|
181
202
|
}
|
|
182
203
|
}
|
|
183
|
-
// ─── Issue #578: Deployment + Host stores ─────────────────────────────────────
|
|
184
|
-
class DeploymentStore {
|
|
185
|
-
constructor(filePath) {
|
|
186
|
-
this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-deployments.json');
|
|
187
|
-
}
|
|
188
|
-
load() {
|
|
189
|
-
try {
|
|
190
|
-
if (!fs_1.default.existsSync(this.filePath))
|
|
191
|
-
return [];
|
|
192
|
-
return JSON.parse(fs_1.default.readFileSync(this.filePath, 'utf8'));
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
save(deployments) {
|
|
199
|
-
fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
|
|
200
|
-
fs_1.default.writeFileSync(this.filePath, JSON.stringify(deployments, null, 2));
|
|
201
|
-
}
|
|
202
|
-
create(deployment) {
|
|
203
|
-
const list = this.load();
|
|
204
|
-
list.push(deployment);
|
|
205
|
-
this.save(list);
|
|
206
|
-
return deployment;
|
|
207
|
-
}
|
|
208
|
-
update(id, updater) {
|
|
209
|
-
const list = this.load();
|
|
210
|
-
const dep = list.find((d) => d.id === id);
|
|
211
|
-
if (!dep)
|
|
212
|
-
return false;
|
|
213
|
-
updater(dep);
|
|
214
|
-
dep.updatedAt = new Date().toISOString();
|
|
215
|
-
this.save(list);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
delete(id) {
|
|
219
|
-
const list = this.load();
|
|
220
|
-
const next = list.filter((d) => d.id !== id);
|
|
221
|
-
if (next.length === list.length)
|
|
222
|
-
return false;
|
|
223
|
-
this.save(next);
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
exports.DeploymentStore = DeploymentStore;
|
|
228
|
-
class HostConfigStore {
|
|
229
|
-
constructor(filePath) {
|
|
230
|
-
this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-hosts.json');
|
|
231
|
-
}
|
|
232
|
-
load() {
|
|
233
|
-
try {
|
|
234
|
-
if (!fs_1.default.existsSync(this.filePath))
|
|
235
|
-
return [];
|
|
236
|
-
return JSON.parse(fs_1.default.readFileSync(this.filePath, 'utf8'));
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
save(hosts) {
|
|
243
|
-
fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
|
|
244
|
-
fs_1.default.writeFileSync(this.filePath, JSON.stringify(hosts, null, 2));
|
|
245
|
-
}
|
|
246
|
-
add(host) {
|
|
247
|
-
const list = this.load();
|
|
248
|
-
list.push(host);
|
|
249
|
-
this.save(list);
|
|
250
|
-
return host;
|
|
251
|
-
}
|
|
252
|
-
delete(id) {
|
|
253
|
-
const list = this.load();
|
|
254
|
-
const next = list.filter((h) => h.id !== id);
|
|
255
|
-
if (next.length === list.length)
|
|
256
|
-
return false;
|
|
257
|
-
this.save(next);
|
|
258
|
-
return true;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
exports.HostConfigStore = HostConfigStore;
|
|
262
|
-
async function pingHost(host) {
|
|
263
|
-
const start = Date.now();
|
|
264
|
-
try {
|
|
265
|
-
const resp = await fetch(`${host.url.replace(/\/$/, '')}/health`, {
|
|
266
|
-
signal: AbortSignal.timeout(5000),
|
|
267
|
-
headers: host.authToken ? { 'X-Hub-Auth': host.authToken } : {},
|
|
268
|
-
});
|
|
269
|
-
const latencyMs = Date.now() - start;
|
|
270
|
-
return {
|
|
271
|
-
id: host.id,
|
|
272
|
-
label: host.label,
|
|
273
|
-
url: host.url,
|
|
274
|
-
status: resp.ok ? 'online' : 'degraded',
|
|
275
|
-
latencyMs,
|
|
276
|
-
lastPingAt: new Date().toISOString(),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return {
|
|
281
|
-
id: host.id,
|
|
282
|
-
label: host.label,
|
|
283
|
-
url: host.url,
|
|
284
|
-
status: 'offline',
|
|
285
|
-
latencyMs: null,
|
|
286
|
-
lastPingAt: new Date().toISOString(),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
204
|
function normalizeReviewArtifact(raw, index = 0) {
|
|
291
205
|
if (!raw || typeof raw !== 'object')
|
|
292
206
|
return null;
|
|
@@ -855,7 +769,6 @@ class AiHubServer {
|
|
|
855
769
|
constructor(options = {}) {
|
|
856
770
|
this.app = (0, express_1.default)();
|
|
857
771
|
this.runRegistry = new AiHubRunRegistry();
|
|
858
|
-
this.cronHandles = new Map();
|
|
859
772
|
this.projectPath = options.projectPath || process.cwd();
|
|
860
773
|
this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
|
|
861
774
|
this.conversationStore = options.conversationStore || new conversation_store_1.AiHubConversationStore();
|
|
@@ -878,9 +791,7 @@ class AiHubServer {
|
|
|
878
791
|
this.dbService = createDefaultDbService();
|
|
879
792
|
this.ownsDbService = this.dbService !== undefined;
|
|
880
793
|
}
|
|
881
|
-
this.
|
|
882
|
-
this.hostConfigStore = options.hostConfigStore ?? new HostConfigStore();
|
|
883
|
-
this.app.use(express_1.default.json({ limit: '10mb' }));
|
|
794
|
+
this.app.use(express_1.default.json());
|
|
884
795
|
if (this.dbService) {
|
|
885
796
|
const { registerPaymentRoutes } = require('../routes/payment-routes');
|
|
886
797
|
registerPaymentRoutes(this.app, () => this.paymentRepo ?? null, this.dbService);
|
|
@@ -1024,8 +935,6 @@ class AiHubServer {
|
|
|
1024
935
|
// so it's known even before the browser is running.
|
|
1025
936
|
process.env.FRAIM_BROWSER_CDP_ENDPOINT = this.managedBrowser.cdpEndpoint();
|
|
1026
937
|
process.env.FRAIM_HUB_BASE_URL = `http://127.0.0.1:${port}`;
|
|
1027
|
-
// Issue #578: rehydrate active scheduled deployments from disk.
|
|
1028
|
-
this.rehydrateScheduledDeployments();
|
|
1029
938
|
// Start HTTPS server when a cert bundle and port are provided.
|
|
1030
939
|
// Word Online requires HTTPS; the HTTPS server shares the same Express app
|
|
1031
940
|
// so all routes (including /word-taskpane/*) are available over both protocols.
|
|
@@ -1065,11 +974,6 @@ class AiHubServer {
|
|
|
1065
974
|
await closeServer(this.httpServer);
|
|
1066
975
|
this.httpServer = undefined;
|
|
1067
976
|
}
|
|
1068
|
-
// Issue #578: stop all active scheduled deployments.
|
|
1069
|
-
for (const [, task] of this.cronHandles) {
|
|
1070
|
-
task.stop();
|
|
1071
|
-
}
|
|
1072
|
-
this.cronHandles.clear();
|
|
1073
977
|
// #521: tear down the shared browser if WE launched it (stop() no-ops on a
|
|
1074
978
|
// browser the manager owns).
|
|
1075
979
|
this.managedBrowser.stop();
|
|
@@ -1094,7 +998,8 @@ class AiHubServer {
|
|
|
1094
998
|
}
|
|
1095
999
|
}
|
|
1096
1000
|
const project = (0, catalog_1.summarizeProject)(normalizedProjectPath);
|
|
1097
|
-
const
|
|
1001
|
+
const catalogOptions = { includeRegistry: true };
|
|
1002
|
+
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath, catalogOptions);
|
|
1098
1003
|
// Issue #566 (R7): jobs already carry `personalized` from catalog discovery
|
|
1099
1004
|
// (true for the fraim/personalized-employee layer). The Hub renders a plain
|
|
1100
1005
|
// "Personalized" marking from that flag — no author/attribution is tracked.
|
|
@@ -1102,7 +1007,7 @@ class AiHubServer {
|
|
|
1102
1007
|
...job,
|
|
1103
1008
|
requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
|
|
1104
1009
|
}));
|
|
1105
|
-
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath);
|
|
1010
|
+
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath, catalogOptions);
|
|
1106
1011
|
const { personas, subscriptionActive, workspaceId, userKey } = await this.computePersonas(apiKey || preferences.apiKey);
|
|
1107
1012
|
const managerTeam = await this.computeManagerTeam(workspaceId, userKey);
|
|
1108
1013
|
// Issue #347: enrich the activeRun the same way GET /runs/:id does
|
|
@@ -1114,7 +1019,7 @@ class AiHubServer {
|
|
|
1114
1019
|
title: 'AI Hub',
|
|
1115
1020
|
project,
|
|
1116
1021
|
preferences,
|
|
1117
|
-
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath),
|
|
1022
|
+
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath, catalogOptions),
|
|
1118
1023
|
jobs,
|
|
1119
1024
|
managerTemplates,
|
|
1120
1025
|
employees,
|
|
@@ -1165,8 +1070,6 @@ class AiHubServer {
|
|
|
1165
1070
|
reviewHandoff: run.reviewHandoff || null,
|
|
1166
1071
|
compareMode: run.runRole === 'fraim' && run.compareRunId ? 'ab' : undefined,
|
|
1167
1072
|
compareRunId: run.compareRunId || null,
|
|
1168
|
-
// Issue #578: preserve trigger source so the UI can render the chip.
|
|
1169
|
-
sourceTrigger: run.sourceTrigger,
|
|
1170
1073
|
};
|
|
1171
1074
|
}
|
|
1172
1075
|
persistRunConversation(run, activeId) {
|
|
@@ -1293,7 +1196,7 @@ class AiHubServer {
|
|
|
1293
1196
|
key: bundle.personaKey,
|
|
1294
1197
|
displayName: bundle.catalogMetadata.displayName,
|
|
1295
1198
|
role: bundle.catalogMetadata.role,
|
|
1296
|
-
avatarUrl: (
|
|
1199
|
+
avatarUrl: buildHubPersonaAvatarUrl(bundle.personaKey),
|
|
1297
1200
|
pricingLabel: bundle.catalogMetadata.pricingLabel,
|
|
1298
1201
|
status: 'locked',
|
|
1299
1202
|
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
@@ -1331,7 +1234,7 @@ class AiHubServer {
|
|
|
1331
1234
|
key: bundle.personaKey,
|
|
1332
1235
|
displayName: bundle.catalogMetadata.displayName,
|
|
1333
1236
|
role: bundle.catalogMetadata.role,
|
|
1334
|
-
avatarUrl: (
|
|
1237
|
+
avatarUrl: buildHubPersonaAvatarUrl(bundle.personaKey),
|
|
1335
1238
|
pricingLabel: hiredKeys.has(bundle.personaKey) ? '' : bundle.catalogMetadata.pricingLabel,
|
|
1336
1239
|
status: (hiredKeys.has(bundle.personaKey) ? 'hired' : 'locked'),
|
|
1337
1240
|
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
@@ -1674,12 +1577,14 @@ class AiHubServer {
|
|
|
1674
1577
|
? path_1.default.resolve(body.projectPath)
|
|
1675
1578
|
: this.projectPath;
|
|
1676
1579
|
const loc = (0, learning_context_builder_1.resolveTeamContextFile)(projectPath, body.key);
|
|
1677
|
-
if (loc.managedByOrgSync || !loc.writePath) {
|
|
1580
|
+
if (loc.managedByOrgSync || loc.managedByManagerSync || !loc.writePath) {
|
|
1678
1581
|
// Enforcement only: block editing a synced org file (it would be
|
|
1679
1582
|
// overwritten on next sync). The how-to-change procedure lives in the
|
|
1680
1583
|
// organization-onboarding job, not in this error body (issue #563 review).
|
|
1681
1584
|
return res.status(409).json({
|
|
1682
|
-
error:
|
|
1585
|
+
error: loc.managedByManagerSync
|
|
1586
|
+
? 'This manager file is managed by manager sync and is read-only here.'
|
|
1587
|
+
: 'This organization file is managed by org sync and is read-only here.'
|
|
1683
1588
|
});
|
|
1684
1589
|
}
|
|
1685
1590
|
const dest = path_1.default.resolve(loc.writePath);
|
|
@@ -2250,27 +2155,6 @@ class AiHubServer {
|
|
|
2250
2155
|
runs = runs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit);
|
|
2251
2156
|
return res.json(runs.map((r) => this.enrichRunForResponse(r)));
|
|
2252
2157
|
});
|
|
2253
|
-
// ─── Issue #578: /api/ai-hub/runs/merged must be registered BEFORE :runId ──
|
|
2254
|
-
this.app.get('/api/ai-hub/runs/merged', async (req, res) => {
|
|
2255
|
-
const local = this.runRegistry.all().map((r) => this.enrichRunForResponse(r));
|
|
2256
|
-
const hosts = this.hostConfigStore.load();
|
|
2257
|
-
const remoteResults = await Promise.allSettled(hosts.map(async (host) => {
|
|
2258
|
-
const url = `${host.url.replace(/\/$/, '')}/api/ai-hub/runs`;
|
|
2259
|
-
const resp = await fetch(url, {
|
|
2260
|
-
signal: AbortSignal.timeout(8000),
|
|
2261
|
-
headers: host.authToken ? { 'X-Hub-Auth': host.authToken } : {},
|
|
2262
|
-
});
|
|
2263
|
-
if (!resp.ok)
|
|
2264
|
-
return [];
|
|
2265
|
-
return resp.json();
|
|
2266
|
-
}));
|
|
2267
|
-
const remote = remoteResults
|
|
2268
|
-
.filter((r) => r.status === 'fulfilled')
|
|
2269
|
-
.flatMap((r) => r.value);
|
|
2270
|
-
const merged = [...local, ...remote].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2271
|
-
return res.json(merged);
|
|
2272
|
-
});
|
|
2273
|
-
// GET /api/ai-hub/runs/:runId — registered AFTER /merged to avoid shadowing.
|
|
2274
2158
|
this.app.get('/api/ai-hub/runs/:runId', (req, res) => {
|
|
2275
2159
|
const run = this.runRegistry.get(req.params.runId);
|
|
2276
2160
|
if (!run) {
|
|
@@ -2278,151 +2162,6 @@ class AiHubServer {
|
|
|
2278
2162
|
}
|
|
2279
2163
|
return res.json(this.enrichRunForResponse(run));
|
|
2280
2164
|
});
|
|
2281
|
-
// ─── Issue #578: Scheduled + Reactive Employees ───────────────────────────
|
|
2282
|
-
// POST /api/ai-hub/schedules — create a recurring scheduled deployment.
|
|
2283
|
-
this.app.post('/api/ai-hub/schedules', (req, res) => {
|
|
2284
|
-
const { label, jobId, projectPath, hostId, cronExpr, instructions, outputChannel, allowConcurrent } = req.body ?? {};
|
|
2285
|
-
if (!label || !jobId || !cronExpr) {
|
|
2286
|
-
return res.status(400).json({ error: 'label, jobId, and cronExpr are required.' });
|
|
2287
|
-
}
|
|
2288
|
-
const validEmployees = ['codex', 'claude', 'gemini', 'copilot'];
|
|
2289
|
-
const resolvedHostId = validEmployees.includes(hostId) ? hostId : 'claude';
|
|
2290
|
-
const now = new Date().toISOString();
|
|
2291
|
-
const deployment = {
|
|
2292
|
-
id: (0, crypto_1.randomUUID)(),
|
|
2293
|
-
type: 'scheduled',
|
|
2294
|
-
label,
|
|
2295
|
-
jobId,
|
|
2296
|
-
projectPath: ensureDirectoryPath(projectPath || this.projectPath),
|
|
2297
|
-
hostId: resolvedHostId,
|
|
2298
|
-
cronExpr,
|
|
2299
|
-
instructions: typeof instructions === 'string' ? instructions : undefined,
|
|
2300
|
-
outputChannel: typeof outputChannel === 'string' ? outputChannel : undefined,
|
|
2301
|
-
allowConcurrent: allowConcurrent === true,
|
|
2302
|
-
active: true,
|
|
2303
|
-
createdAt: now,
|
|
2304
|
-
updatedAt: now,
|
|
2305
|
-
};
|
|
2306
|
-
this.deploymentStore.create(deployment);
|
|
2307
|
-
this.scheduleDeployment(deployment);
|
|
2308
|
-
return res.status(201).json(deployment);
|
|
2309
|
-
});
|
|
2310
|
-
// GET /api/ai-hub/schedules — list all scheduled deployments.
|
|
2311
|
-
this.app.get('/api/ai-hub/schedules', (_req, res) => {
|
|
2312
|
-
return res.json(this.deploymentStore.load().filter((d) => d.type === 'scheduled'));
|
|
2313
|
-
});
|
|
2314
|
-
// DELETE /api/ai-hub/schedules/:id — remove a scheduled deployment.
|
|
2315
|
-
this.app.delete('/api/ai-hub/schedules/:id', (req, res) => {
|
|
2316
|
-
const { id } = req.params;
|
|
2317
|
-
const task = this.cronHandles.get(id);
|
|
2318
|
-
if (task) {
|
|
2319
|
-
task.stop();
|
|
2320
|
-
this.cronHandles.delete(id);
|
|
2321
|
-
}
|
|
2322
|
-
const deleted = this.deploymentStore.delete(id);
|
|
2323
|
-
if (!deleted)
|
|
2324
|
-
return res.status(404).json({ error: 'Deployment not found.' });
|
|
2325
|
-
return res.json({ ok: true });
|
|
2326
|
-
});
|
|
2327
|
-
// POST /api/ai-hub/webhooks — register an inbound webhook deployment.
|
|
2328
|
-
this.app.post('/api/ai-hub/webhooks', (req, res) => {
|
|
2329
|
-
const { label, jobId, projectPath, hostId, instructions, outputChannel, allowConcurrent } = req.body ?? {};
|
|
2330
|
-
if (!label || !jobId) {
|
|
2331
|
-
return res.status(400).json({ error: 'label and jobId are required.' });
|
|
2332
|
-
}
|
|
2333
|
-
const validEmployees = ['codex', 'claude', 'gemini', 'copilot'];
|
|
2334
|
-
const resolvedHostId = validEmployees.includes(hostId) ? hostId : 'claude';
|
|
2335
|
-
const now = new Date().toISOString();
|
|
2336
|
-
const deployment = {
|
|
2337
|
-
id: (0, crypto_1.randomUUID)(),
|
|
2338
|
-
type: 'webhook',
|
|
2339
|
-
label,
|
|
2340
|
-
jobId,
|
|
2341
|
-
projectPath: ensureDirectoryPath(projectPath || this.projectPath),
|
|
2342
|
-
hostId: resolvedHostId,
|
|
2343
|
-
instructions: typeof instructions === 'string' ? instructions : undefined,
|
|
2344
|
-
outputChannel: typeof outputChannel === 'string' ? outputChannel : undefined,
|
|
2345
|
-
allowConcurrent: allowConcurrent === true,
|
|
2346
|
-
active: true,
|
|
2347
|
-
createdAt: now,
|
|
2348
|
-
updatedAt: now,
|
|
2349
|
-
};
|
|
2350
|
-
this.deploymentStore.create(deployment);
|
|
2351
|
-
const hubBase = process.env.FRAIM_HUB_BASE_URL || `http://127.0.0.1:${this.httpPort}`;
|
|
2352
|
-
return res.status(201).json({ ...deployment, inboundUrl: `${hubBase}/api/ai-hub/webhooks/${deployment.id}/inbound` });
|
|
2353
|
-
});
|
|
2354
|
-
// GET /api/ai-hub/webhooks — list all webhook deployments.
|
|
2355
|
-
this.app.get('/api/ai-hub/webhooks', (_req, res) => {
|
|
2356
|
-
const hubBase = process.env.FRAIM_HUB_BASE_URL || `http://127.0.0.1:${this.httpPort}`;
|
|
2357
|
-
return res.json(this.deploymentStore.load()
|
|
2358
|
-
.filter((d) => d.type === 'webhook')
|
|
2359
|
-
.map((d) => ({ ...d, inboundUrl: `${hubBase}/api/ai-hub/webhooks/${d.id}/inbound` })));
|
|
2360
|
-
});
|
|
2361
|
-
// DELETE /api/ai-hub/webhooks/:id — remove a webhook deployment.
|
|
2362
|
-
this.app.delete('/api/ai-hub/webhooks/:id', (req, res) => {
|
|
2363
|
-
const deleted = this.deploymentStore.delete(req.params.id);
|
|
2364
|
-
if (!deleted)
|
|
2365
|
-
return res.status(404).json({ error: 'Deployment not found.' });
|
|
2366
|
-
return res.json({ ok: true });
|
|
2367
|
-
});
|
|
2368
|
-
// POST /api/ai-hub/webhooks/:id/inbound — webhook inbound trigger from external systems.
|
|
2369
|
-
this.app.post('/api/ai-hub/webhooks/:id/inbound', async (req, res) => {
|
|
2370
|
-
const deployments = this.deploymentStore.load();
|
|
2371
|
-
const deployment = deployments.find((d) => d.id === req.params.id && d.type === 'webhook' && d.active);
|
|
2372
|
-
if (!deployment) {
|
|
2373
|
-
return res.status(404).json({ error: 'Webhook not found or inactive.' });
|
|
2374
|
-
}
|
|
2375
|
-
try {
|
|
2376
|
-
const run = await this.fireDeploymentRun(deployment, req.body);
|
|
2377
|
-
return res.status(202).json({ runId: run.id, status: run.status });
|
|
2378
|
-
}
|
|
2379
|
-
catch (err) {
|
|
2380
|
-
const msg = err instanceof Error ? err.message : 'Failed to start run.';
|
|
2381
|
-
return res.status(500).json({ error: msg });
|
|
2382
|
-
}
|
|
2383
|
-
});
|
|
2384
|
-
// GET /api/ai-hub/hosts — list registered remote hosts with health status.
|
|
2385
|
-
this.app.get('/api/ai-hub/hosts', async (_req, res) => {
|
|
2386
|
-
const hosts = this.hostConfigStore.load();
|
|
2387
|
-
const healthResults = await Promise.allSettled(hosts.map((h) => pingHost(h)));
|
|
2388
|
-
const health = healthResults.map((r, i) => r.status === 'fulfilled'
|
|
2389
|
-
? r.value
|
|
2390
|
-
: { id: hosts[i].id, label: hosts[i].label, url: hosts[i].url, status: 'offline', latencyMs: null, lastPingAt: new Date().toISOString() });
|
|
2391
|
-
return res.json(health);
|
|
2392
|
-
});
|
|
2393
|
-
// POST /api/ai-hub/hosts — register a named remote hub host.
|
|
2394
|
-
this.app.post('/api/ai-hub/hosts', (req, res) => {
|
|
2395
|
-
const { label, url, authToken } = req.body ?? {};
|
|
2396
|
-
const validUrl = safeHttpUrl(url);
|
|
2397
|
-
if (!label || !validUrl) {
|
|
2398
|
-
return res.status(400).json({ error: 'label and a valid http(s) url are required.' });
|
|
2399
|
-
}
|
|
2400
|
-
const host = {
|
|
2401
|
-
id: (0, crypto_1.randomUUID)(),
|
|
2402
|
-
label,
|
|
2403
|
-
url: validUrl,
|
|
2404
|
-
authToken: typeof authToken === 'string' && authToken ? authToken : undefined,
|
|
2405
|
-
createdAt: new Date().toISOString(),
|
|
2406
|
-
};
|
|
2407
|
-
this.hostConfigStore.add(host);
|
|
2408
|
-
return res.status(201).json({ id: host.id, label: host.label, url: host.url, createdAt: host.createdAt });
|
|
2409
|
-
});
|
|
2410
|
-
// DELETE /api/ai-hub/hosts/:id — remove a named remote host.
|
|
2411
|
-
this.app.delete('/api/ai-hub/hosts/:id', (req, res) => {
|
|
2412
|
-
const deleted = this.hostConfigStore.delete(req.params.id);
|
|
2413
|
-
if (!deleted)
|
|
2414
|
-
return res.status(404).json({ error: 'Host not found.' });
|
|
2415
|
-
return res.json({ ok: true });
|
|
2416
|
-
});
|
|
2417
|
-
// GET /api/ai-hub/hosts/:id/health — ping a single host.
|
|
2418
|
-
this.app.get('/api/ai-hub/hosts/:id/health', async (req, res) => {
|
|
2419
|
-
const host = this.hostConfigStore.load().find((h) => h.id === req.params.id);
|
|
2420
|
-
if (!host)
|
|
2421
|
-
return res.status(404).json({ error: 'Host not found.' });
|
|
2422
|
-
const health = await pingHost(host);
|
|
2423
|
-
return res.json(health);
|
|
2424
|
-
});
|
|
2425
|
-
// ─── End Issue #578 ───────────────────────────────────────────────────────
|
|
2426
2165
|
// -------------------------------------------------------------------------
|
|
2427
2166
|
// Issue #489: POST /api/trigger
|
|
2428
2167
|
// Stable API endpoint for extension surfaces (Office add-ins, browser
|
|
@@ -2521,117 +2260,6 @@ class AiHubServer {
|
|
|
2521
2260
|
process.env.FRAIM_BROWSER_CDP_ENDPOINT = result.endpoint;
|
|
2522
2261
|
return { endpoint: result.endpoint, reused: result.reused, channel: result.channel };
|
|
2523
2262
|
}
|
|
2524
|
-
// ─── Issue #578: Scheduled deployment helpers ─────────────────────────────
|
|
2525
|
-
rehydrateScheduledDeployments() {
|
|
2526
|
-
const deployments = this.deploymentStore.load();
|
|
2527
|
-
for (const dep of deployments) {
|
|
2528
|
-
if (dep.type === 'scheduled' && dep.active) {
|
|
2529
|
-
this.scheduleDeployment(dep);
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
if (deployments.filter((d) => d.type === 'scheduled' && d.active).length > 0) {
|
|
2533
|
-
console.log(`[ai-hub] rehydrated ${deployments.filter((d) => d.type === 'scheduled' && d.active).length} scheduled deployment(s)`);
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
scheduleDeployment(deployment) {
|
|
2537
|
-
if (!deployment.cronExpr)
|
|
2538
|
-
return;
|
|
2539
|
-
try {
|
|
2540
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
2541
|
-
const cron = require('node-cron');
|
|
2542
|
-
if (!cron.validate(deployment.cronExpr)) {
|
|
2543
|
-
console.warn(`[ai-hub] invalid cronExpr for deployment ${deployment.id}: ${deployment.cronExpr}`);
|
|
2544
|
-
return;
|
|
2545
|
-
}
|
|
2546
|
-
const task = cron.schedule(deployment.cronExpr, async () => {
|
|
2547
|
-
try {
|
|
2548
|
-
await this.fireDeploymentRun(deployment);
|
|
2549
|
-
}
|
|
2550
|
-
catch (err) {
|
|
2551
|
-
console.warn(`[ai-hub] scheduled deployment ${deployment.id} fire failed:`, err);
|
|
2552
|
-
}
|
|
2553
|
-
});
|
|
2554
|
-
this.cronHandles.set(deployment.id, task);
|
|
2555
|
-
}
|
|
2556
|
-
catch (err) {
|
|
2557
|
-
console.warn('[ai-hub] node-cron not available — scheduled deployments require node-cron:', err);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
async fireDeploymentRun(deployment, webhookBody) {
|
|
2561
|
-
// Overlapping run guard: if the prior run is still active and allowConcurrent is false, skip.
|
|
2562
|
-
if (!deployment.allowConcurrent && deployment.activeRunId) {
|
|
2563
|
-
const active = this.runRegistry.get(deployment.activeRunId);
|
|
2564
|
-
if (active && active.status === 'running') {
|
|
2565
|
-
console.log(`[ai-hub] deployment ${deployment.id} skipped — prior run ${deployment.activeRunId} still running`);
|
|
2566
|
-
return active;
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
const employees = this.hostRuntime.detectEmployees();
|
|
2570
|
-
const employee = employees.find((e) => e.id === deployment.hostId);
|
|
2571
|
-
if (!employee?.available) {
|
|
2572
|
-
throw new Error(`Employee ${deployment.hostId} is not available for scheduled/webhook run.`);
|
|
2573
|
-
}
|
|
2574
|
-
const instructions = [
|
|
2575
|
-
deployment.instructions ?? `/fraim ${deployment.jobId}`,
|
|
2576
|
-
webhookBody ? `\n\nInbound payload:\n${JSON.stringify(webhookBody, null, 2)}` : '',
|
|
2577
|
-
].join('').trim();
|
|
2578
|
-
const startTimestamp = new Date().toISOString();
|
|
2579
|
-
const run = {
|
|
2580
|
-
id: (0, crypto_1.randomUUID)(),
|
|
2581
|
-
jobId: deployment.jobId,
|
|
2582
|
-
hostId: deployment.hostId,
|
|
2583
|
-
projectPath: deployment.projectPath,
|
|
2584
|
-
status: 'running',
|
|
2585
|
-
sourceTrigger: deployment.type === 'scheduled' ? 'scheduled' : 'webhook',
|
|
2586
|
-
createdAt: startTimestamp,
|
|
2587
|
-
updatedAt: startTimestamp,
|
|
2588
|
-
messages: [(0, hosts_1.createHubMessage)('manager', instructions)],
|
|
2589
|
-
events: [(0, hosts_1.createHubEvent)('system', `Triggered by deployment: ${deployment.label} (${deployment.type})`)],
|
|
2590
|
-
currentPhase: null,
|
|
2591
|
-
phaseHistory: [],
|
|
2592
|
-
totals: emptyTotals(),
|
|
2593
|
-
lastStatusChangeAt: startTimestamp,
|
|
2594
|
-
personaKey: getProtectedPersonaForHubJob(deployment.jobId),
|
|
2595
|
-
};
|
|
2596
|
-
const child = this.hostRuntime.startRun(deployment.hostId, deployment.projectPath, instructions, {
|
|
2597
|
-
onEvent: (event, channel) => {
|
|
2598
|
-
this.runRegistry.update(run.id, (current) => {
|
|
2599
|
-
if (event.sessionId)
|
|
2600
|
-
current.sessionId = event.sessionId;
|
|
2601
|
-
appendHostMessage(current, deployment.hostId, event, channel);
|
|
2602
|
-
if (event.raw) {
|
|
2603
|
-
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
2604
|
-
applyReviewProjection(current, event.raw);
|
|
2605
|
-
}
|
|
2606
|
-
if (event.agentIdentity)
|
|
2607
|
-
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
2608
|
-
if (event.seekMentoring)
|
|
2609
|
-
applySeekMentoringSignal(current, event.seekMentoring);
|
|
2610
|
-
if (event.usage)
|
|
2611
|
-
applyUsageSignal(current, event.usage);
|
|
2612
|
-
});
|
|
2613
|
-
const updated = this.runRegistry.get(run.id);
|
|
2614
|
-
if (updated)
|
|
2615
|
-
this.persistRunConversation(updated, updated.conversationId || updated.id);
|
|
2616
|
-
},
|
|
2617
|
-
onExit: (exitCode) => {
|
|
2618
|
-
this.runRegistry.update(run.id, (r) => {
|
|
2619
|
-
r.exitCode = exitCode;
|
|
2620
|
-
r.status = exitCode === 0 ? 'completed' : 'failed';
|
|
2621
|
-
r.events.push((0, hosts_1.createHubEvent)('system', `Run exited with code ${exitCode ?? 'unknown'}.`));
|
|
2622
|
-
});
|
|
2623
|
-
const updated = this.runRegistry.get(run.id);
|
|
2624
|
-
if (updated)
|
|
2625
|
-
this.persistRunConversation(updated, updated.conversationId || updated.id);
|
|
2626
|
-
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
|
|
2627
|
-
this.runRegistry.dispose(run.id);
|
|
2628
|
-
},
|
|
2629
|
-
});
|
|
2630
|
-
this.runRegistry.create(run, child);
|
|
2631
|
-
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = run.id; });
|
|
2632
|
-
return run;
|
|
2633
|
-
}
|
|
2634
|
-
// ─── End Issue #578 helpers ───────────────────────────────────────────────
|
|
2635
2263
|
// Issue #347 — assemble the read-side projection of a run. Stages are
|
|
2636
2264
|
// derived from job frontmatter + visited phases; totalDurationMs ticks
|
|
2637
2265
|
// forward while the run is still running so the UI's totals line
|