fraim-hub 2.0.207 → 2.0.208

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/bin/fraim-hub.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  try {
4
4
  const { createFraimHubProgram } = require('../dist/src/cli/fraim-hub.js');
@@ -80,7 +80,10 @@ function openDesktopWindow(projectPath, preferredPort) {
80
80
  if (!electronBinary || !desktopEntry) {
81
81
  return false;
82
82
  }
83
- const child = (0, child_process_1.spawn)(electronBinary, [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)], {
83
+ const args = projectPath
84
+ ? [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)]
85
+ : [desktopEntry, '--port', String(preferredPort)];
86
+ const child = (0, child_process_1.spawn)(electronBinary, args, {
84
87
  detached: true,
85
88
  stdio: 'ignore',
86
89
  });
@@ -182,7 +185,7 @@ async function reconcileRunningHub(flags) {
182
185
  async function runHub(options) {
183
186
  const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('./server')));
184
187
  const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
185
- const projectPath = path_1.default.resolve(options.projectPath || process.cwd());
188
+ const projectPath = options.projectPath ? path_1.default.resolve(options.projectPath) : undefined;
186
189
  if (options.open) {
187
190
  const wantDesktop = !options.browser;
188
191
  if (wantDesktop) {
@@ -191,22 +194,25 @@ async function runHub(options) {
191
194
  const openedDesktop = wantDesktop && openDesktopWindow(projectPath, preferredPort);
192
195
  if (!openedDesktop) {
193
196
  const port = await findAvailablePort(preferredPort);
194
- const server = new AiHubServer({ projectPath });
197
+ const server = new AiHubServer(projectPath ? { projectPath } : {});
195
198
  await server.start(port);
196
199
  const url = `http://127.0.0.1:${port}/ai-hub/`;
197
200
  console.log(`AI Hub running at ${url}`);
198
- console.log(`Project path: ${projectPath}`);
201
+ console.log(`Project path: ${server.getProjectPath()}`);
199
202
  openBrowser(url);
200
203
  return;
201
204
  }
202
205
  console.log('AI Hub desktop shell launched.');
203
- console.log(`Project path: ${projectPath}`);
206
+ if (projectPath)
207
+ console.log(`Project path: ${projectPath}`);
208
+ else
209
+ console.log('Project path: loaded from saved Hub state');
204
210
  return;
205
211
  }
206
212
  const port = await findAvailablePort(preferredPort);
207
- const server = new AiHubServer({ projectPath });
213
+ const server = new AiHubServer(projectPath ? { projectPath } : {});
208
214
  await server.start(port);
209
215
  const url = `http://127.0.0.1:${port}/ai-hub/`;
210
216
  console.log(`AI Hub running at ${url}`);
211
- console.log(`Project path: ${projectPath}`);
217
+ console.log(`Project path: ${server.getProjectPath()}`);
212
218
  }
@@ -33,7 +33,7 @@ function preferredWindowSize() {
33
33
  };
34
34
  }
35
35
  function parseArgs(argv) {
36
- let projectPath = process.cwd();
36
+ let projectPath;
37
37
  let preferredPort = 43091;
38
38
  for (let i = 0; i < argv.length; i += 1) {
39
39
  if (argv[i] === '--project-path' && argv[i + 1]) {
@@ -355,7 +355,7 @@ async function launchDesktopShell(options) {
355
355
  // Fast on subsequent launches (file read); ~200ms on first launch (key gen).
356
356
  const certBundle = await (0, cert_store_1.loadOrCreateCert)();
357
357
  server = new server_1.AiHubServer({
358
- projectPath: options.projectPath,
358
+ ...(options.projectPath ? { projectPath: options.projectPath } : {}),
359
359
  // Issue #701: no local DB. Persona/manager-team state resolves through the hosted
360
360
  // server via the Hub's default remote gateway (authenticated by the user's API key).
361
361
  httpsPort,
@@ -370,6 +370,7 @@ async function launchDesktopShell(options) {
370
370
  },
371
371
  });
372
372
  await server.start(httpPort);
373
+ const resolvedProjectPath = server.getProjectPath();
373
374
  // #755: record the live instance so `fraim hub` can detect a stale build and
374
375
  // replace it instead of re-focusing an outdated process.
375
376
  try {
@@ -383,7 +384,7 @@ async function launchDesktopShell(options) {
383
384
  catch (err) {
384
385
  console.warn('[fraim] could not write hub-runtime.json:', err);
385
386
  }
386
- ensureWordSideload(options.projectPath, httpPort);
387
+ ensureWordSideload(resolvedProjectPath, httpPort);
387
388
  const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
388
389
  createTray(hubUrl);
389
390
  await createWindow(hubUrl);
@@ -130,6 +130,12 @@ function loadPersonaCapabilityModule() {
130
130
  function getProtectedPersonaForHubJob(jobName) {
131
131
  return loadPersonaCapabilityModule()?.getProtectedPersonaForJob(jobName) ?? null;
132
132
  }
133
+ const GENERIC_WORKER_PERSONA_KEY = 'fraimworker';
134
+ function getHubPersonaForJob(jobName) {
135
+ if (!jobName || jobName === '__freeform__')
136
+ return null;
137
+ return getProtectedPersonaForHubJob(jobName) ?? GENERIC_WORKER_PERSONA_KEY;
138
+ }
133
139
  const FRAIM_INTERNAL_JOB_IDS = new Set([
134
140
  'contribute-to-fraim',
135
141
  'create-registry-asset',
@@ -177,6 +183,24 @@ function buildHubManagerHiringCatalog() {
177
183
  roles: {},
178
184
  };
179
185
  }
186
+ function directoryExists(projectPath) {
187
+ try {
188
+ return fs_1.default.statSync(projectPath).isDirectory();
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ function resolveInitialHubProjectPath(preferencesStore, fallbackPath) {
195
+ const fallback = path_1.default.resolve(fallbackPath || process.cwd());
196
+ const recorded = preferencesStore.load(fallback).projectPath;
197
+ if (typeof recorded === 'string' && recorded.trim().length > 0) {
198
+ const resolved = path_1.default.resolve(recorded);
199
+ if (directoryExists(resolved))
200
+ return resolved;
201
+ }
202
+ return fallback;
203
+ }
180
204
  class AiHubRunRegistry {
181
205
  constructor() {
182
206
  this.runs = new Map();
@@ -1185,8 +1209,10 @@ class AiHubServer {
1185
1209
  this.app = (0, express_1.default)();
1186
1210
  this.runRegistry = new AiHubRunRegistry();
1187
1211
  this.cronHandles = new Map();
1188
- this.projectPath = options.projectPath || process.cwd();
1189
1212
  this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
1213
+ this.projectPath = options.projectPath
1214
+ ? path_1.default.resolve(options.projectPath)
1215
+ : resolveInitialHubProjectPath(this.preferencesStore, process.cwd());
1190
1216
  this.conversationStore = options.conversationStore || new conversation_store_1.AiHubConversationStore();
1191
1217
  this.configuredAgentStore = options.configuredAgentStore || new configured_agents_1.AiHubConfiguredAgentStore();
1192
1218
  this.wordTaskpaneDir = options.wordTaskpaneDir ?? resolveWordTaskpaneDir(this.projectPath);
@@ -1325,6 +1351,12 @@ class AiHubServer {
1325
1351
  getApp() {
1326
1352
  return this.app;
1327
1353
  }
1354
+ getProjectPath() {
1355
+ return this.projectPath;
1356
+ }
1357
+ defaultProjectPath() {
1358
+ return resolveInitialHubProjectPath(this.preferencesStore, this.projectPath);
1359
+ }
1328
1360
  async start(port) {
1329
1361
  this.httpPort = port;
1330
1362
  await new Promise((resolve, reject) => {
@@ -1946,7 +1978,7 @@ class AiHubServer {
1946
1978
  id: employeeJob.id,
1947
1979
  title: employeeJob.title,
1948
1980
  stubPath: employeeJob.stubPath,
1949
- personaKey: employeeJob.requiredPersonaKey ?? getProtectedPersonaForHubJob(employeeJob.id),
1981
+ personaKey: employeeJob.requiredPersonaKey ?? getHubPersonaForJob(employeeJob.id),
1950
1982
  };
1951
1983
  }
1952
1984
  const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
@@ -1955,7 +1987,7 @@ class AiHubServer {
1955
1987
  id: managerTemplate.id,
1956
1988
  title: managerTemplate.title,
1957
1989
  stubPath: managerTemplate.stubPath,
1958
- personaKey: getProtectedPersonaForHubJob(managerTemplate.id),
1990
+ personaKey: getHubPersonaForJob(managerTemplate.id),
1959
1991
  };
1960
1992
  }
1961
1993
  return null;
@@ -2228,20 +2260,9 @@ class AiHubServer {
2228
2260
  }
2229
2261
  catch { }
2230
2262
  }
2231
- // #719: a bare reload (no projectPath query) must land on the last-recorded
2232
- // workspace project, not the launch folder. The bootstrap saves its resolved
2233
- // current path back into the projects list, so defaulting to the launch
2234
- // folder would resurrect a removed launch-folder project on every reload
2235
- // (tfSwitchProjectFolder already documents the recorded folder as "the
2236
- // default on next load"). The launch folder stays the fallback when nothing
2237
- // is recorded or the recorded folder no longer exists.
2238
- if (!projectPath) {
2239
- const recorded = this.preferencesStore.load(this.projectPath).projectPath;
2240
- if (recorded && path_1.default.resolve(recorded) !== path_1.default.resolve(this.projectPath) && fs_1.default.existsSync(recorded)) {
2241
- projectPath = recorded;
2242
- }
2243
- }
2244
- res.json(await this.bootstrapResponse(projectPath || this.projectPath));
2263
+ // #719 / local Hub package: a bare reload must land on the user-level saved
2264
+ // Hub project, not whatever directory happened to launch the process.
2265
+ res.json(await this.bootstrapResponse(projectPath || this.defaultProjectPath()));
2245
2266
  });
2246
2267
  // Issue #512 (S3, R14) — Brain summary as a standalone route, returning the
2247
2268
  // same projection folded into bootstrap. Useful for the avatar→Brain view
@@ -2249,7 +2270,7 @@ class AiHubServer {
2249
2270
  this.app.get('/api/ai-hub/brain', async (req, res) => {
2250
2271
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2251
2272
  ? path_1.default.resolve(req.query.projectPath)
2252
- : this.projectPath;
2273
+ : this.defaultProjectPath();
2253
2274
  const jobCount = (0, catalog_1.discoverEmployeeJobs)(projectPath).length + (0, catalog_1.discoverManagerTemplates)(projectPath).length;
2254
2275
  const userEmail = await this.resolveHubIdentity();
2255
2276
  return res.json(this.computeBrain(projectPath, jobCount, userEmail));
@@ -2264,7 +2285,7 @@ class AiHubServer {
2264
2285
  ? path_1.default.resolve(req.query.projectPath)
2265
2286
  : (typeof (req.body && req.body.projectPath) === 'string' && req.body.projectPath.length > 0
2266
2287
  ? path_1.default.resolve(req.body.projectPath)
2267
- : this.projectPath);
2288
+ : this.defaultProjectPath());
2268
2289
  this.app.get('/api/ai-hub/learnings', async (req, res) => {
2269
2290
  const scope = req.query.scope;
2270
2291
  if (typeof scope !== 'string' || !VALID_SCOPES.includes(scope)) {
@@ -2344,7 +2365,7 @@ class AiHubServer {
2344
2365
  ? (0, conversation_store_1.conversationScopeKey)(scope, '')
2345
2366
  : (typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2346
2367
  ? path_1.default.resolve(req.query.projectPath)
2347
- : this.projectPath);
2368
+ : this.defaultProjectPath());
2348
2369
  const loaded = this.conversationStore.loadProject(key);
2349
2370
  return res.json({ projectPath: key, scope: scope ?? 'project', ...loaded, source: 'disk' });
2350
2371
  });
@@ -2352,7 +2373,10 @@ class AiHubServer {
2352
2373
  try {
2353
2374
  const body = (req.body ?? {});
2354
2375
  const scope = scopeParam(body.scope);
2355
- const key = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
2376
+ if (!scope && (typeof body.projectPath !== 'string' || body.projectPath.trim().length === 0)) {
2377
+ return res.status(400).json({ error: 'projectPath required for project conversations' });
2378
+ }
2379
+ const key = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath);
2356
2380
  if (!Array.isArray(body.conversations)) {
2357
2381
  return res.status(400).json({ error: 'conversations array required' });
2358
2382
  }
@@ -2401,7 +2425,10 @@ class AiHubServer {
2401
2425
  try {
2402
2426
  const body = (req.body ?? {});
2403
2427
  const scope = scopeParam(body.scope);
2404
- const projectPath = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
2428
+ if (!scope && (typeof body.projectPath !== 'string' || body.projectPath.trim().length === 0)) {
2429
+ return res.status(400).json({ error: 'projectPath required for project conversations' });
2430
+ }
2431
+ const projectPath = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath);
2405
2432
  const saved = this.conversationStore.patchConversation(projectPath, req.params.conversationId, body);
2406
2433
  if (body.activeId !== undefined) {
2407
2434
  const withActive = this.conversationStore.replaceProject(projectPath, { ...saved, activeId: body.activeId });
@@ -2416,13 +2443,13 @@ class AiHubServer {
2416
2443
  this.app.get('/api/ai-hub/projects', (req, res) => {
2417
2444
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2418
2445
  ? path_1.default.resolve(req.query.projectPath)
2419
- : this.projectPath;
2446
+ : this.defaultProjectPath();
2420
2447
  return res.json({ projectPath, projects: this.knownProjects(projectPath), source: 'disk' });
2421
2448
  });
2422
2449
  this.app.put('/api/ai-hub/projects', (req, res) => {
2423
2450
  try {
2424
2451
  const body = (req.body ?? {});
2425
- const projectPath = ensureDirectoryPath(body.projectPath || this.projectPath);
2452
+ const projectPath = ensureDirectoryPath(body.projectPath || this.defaultProjectPath());
2426
2453
  if (!Array.isArray(body.projects)) {
2427
2454
  return res.status(400).json({ error: 'projects array required' });
2428
2455
  }
@@ -2442,7 +2469,7 @@ class AiHubServer {
2442
2469
  this.app.delete('/api/ai-hub/projects/:id', (req, res) => {
2443
2470
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2444
2471
  ? path_1.default.resolve(req.query.projectPath)
2445
- : this.projectPath;
2472
+ : this.defaultProjectPath();
2446
2473
  const known = this.knownProjects(projectPath);
2447
2474
  // Match by id, falling back to the canonical folderPath. The client and server
2448
2475
  // hash a path to an id with different algorithms, and the client mints its own id
@@ -2486,7 +2513,7 @@ class AiHubServer {
2486
2513
  // (set by `fraim setup`), read fresh via resolveApiKey() on every request.
2487
2514
  this.app.post('/api/ai-hub/preferences', (req, res) => {
2488
2515
  const { personaKey } = req.body;
2489
- const prefs = this.preferencesStore.load(this.projectPath);
2516
+ const prefs = this.preferencesStore.load(this.defaultProjectPath());
2490
2517
  this.preferencesStore.save({ ...prefs, personaKey: personaKey ?? null });
2491
2518
  return res.json({ ok: true });
2492
2519
  });
@@ -2544,7 +2571,7 @@ class AiHubServer {
2544
2571
  this.app.get('/api/ai-hub/context', (req, res) => {
2545
2572
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2546
2573
  ? path_1.default.resolve(req.query.projectPath)
2547
- : this.projectPath;
2574
+ : this.defaultProjectPath();
2548
2575
  const rawKey = typeof req.query.key === 'string' ? req.query.key : undefined;
2549
2576
  if (rawKey !== undefined) {
2550
2577
  if (!(0, learning_context_builder_1.isTeamContextKey)(rawKey)) {
@@ -2568,7 +2595,7 @@ class AiHubServer {
2568
2595
  }
2569
2596
  const projectPath = typeof body.projectPath === 'string' && body.projectPath.length > 0
2570
2597
  ? path_1.default.resolve(body.projectPath)
2571
- : this.projectPath;
2598
+ : this.defaultProjectPath();
2572
2599
  const loc = (0, learning_context_builder_1.resolveTeamContextFile)(projectPath, body.key);
2573
2600
  if (loc.managedByOrgSync || loc.managedByManagerSync || !loc.writePath) {
2574
2601
  // Enforcement only: block editing a synced org file (it would be
@@ -2607,14 +2634,14 @@ class AiHubServer {
2607
2634
  this.app.get('/api/ai-hub/brand', (req, res) => {
2608
2635
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
2609
2636
  ? path_1.default.resolve(req.query.projectPath)
2610
- : this.projectPath;
2637
+ : this.defaultProjectPath();
2611
2638
  return res.json({ brand: (0, learning_context_builder_1.readOrgBrand)(projectPath) });
2612
2639
  });
2613
2640
  this.app.post('/api/ai-hub/brand', (req, res) => {
2614
2641
  const body = (req.body ?? {});
2615
2642
  const projectPath = typeof body.projectPath === 'string' && body.projectPath.length > 0
2616
2643
  ? path_1.default.resolve(body.projectPath)
2617
- : this.projectPath;
2644
+ : this.defaultProjectPath();
2618
2645
  const input = {
2619
2646
  name: typeof body.name === 'string' ? body.name : undefined,
2620
2647
  color: typeof body.color === 'string' ? body.color : undefined,
@@ -2640,7 +2667,7 @@ class AiHubServer {
2640
2667
  const rawPath = typeof req.body?.path === 'string' ? req.body.path : '';
2641
2668
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.length > 0
2642
2669
  ? path_1.default.resolve(req.body.projectPath)
2643
- : this.projectPath;
2670
+ : this.defaultProjectPath();
2644
2671
  if (!rawPath)
2645
2672
  return res.status(400).json({ error: 'path is required.' });
2646
2673
  const resolved = resolveSafeArtifactPath(rawPath, projectPath);
@@ -2825,7 +2852,7 @@ class AiHubServer {
2825
2852
  });
2826
2853
  this.app.post('/api/ai-hub/runs', (req, res) => {
2827
2854
  try {
2828
- const projectPath = ensureDirectoryPath(req.body.projectPath || this.projectPath);
2855
+ const projectPath = ensureDirectoryPath(req.body.projectPath || this.defaultProjectPath());
2829
2856
  const requestedHostId = req.body.hostId;
2830
2857
  const instructions = (req.body.instructions || '').trim();
2831
2858
  const legacyMessage = (req.body.message || '').trim();
@@ -2876,7 +2903,7 @@ class AiHubServer {
2876
2903
  phaseHistory: [],
2877
2904
  totals: emptyTotals(),
2878
2905
  lastStatusChangeAt: startTimestamp,
2879
- personaKey: jobMetadata?.personaKey ?? getProtectedPersonaForHubJob(jobId),
2906
+ personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
2880
2907
  // Issue #442: mark this as the FRAIM side of an A/B pair when applicable.
2881
2908
  ...(compareMode === 'ab' ? { runRole: 'fraim' } : {}),
2882
2909
  // #0: trigger source — defaults to 'manager' when not provided by the caller.
@@ -3172,7 +3199,7 @@ class AiHubServer {
3172
3199
  this.app.post('/api/ai-hub/runs/resume', (req, res) => {
3173
3200
  try {
3174
3201
  const body = (req.body ?? {});
3175
- const projectPath = ensureDirectoryPath(body.projectPath || this.projectPath);
3202
+ const projectPath = ensureDirectoryPath(body.projectPath || this.defaultProjectPath());
3176
3203
  const requestedHostId = body.hostId;
3177
3204
  const sessionId = (body.sessionId || '').trim();
3178
3205
  const jobId = (body.jobId || '').trim();
@@ -3206,7 +3233,7 @@ class AiHubServer {
3206
3233
  totals: persistedRun?.totals || emptyTotals(),
3207
3234
  lastStatusChangeAt: now,
3208
3235
  runDiscriminant: persistedRun?.runDiscriminant || undefined,
3209
- personaKey: getProtectedPersonaForHubJob(jobId),
3236
+ personaKey: getHubPersonaForJob(jobId),
3210
3237
  };
3211
3238
  // Continue-turn message (FRAIM invocation for the job + instructions) plus
3212
3239
  // the shared-browser note so the resumed agent knows about it.
@@ -3378,7 +3405,7 @@ class AiHubServer {
3378
3405
  type: 'scheduled',
3379
3406
  label,
3380
3407
  jobId,
3381
- projectPath: ensureDirectoryPath(projectPath || this.projectPath),
3408
+ projectPath: ensureDirectoryPath(projectPath || this.defaultProjectPath()),
3382
3409
  hostId: resolvedHostId,
3383
3410
  ...(typeof configuredAgentId === 'string' && configuredAgentId.trim() ? { configuredAgentId: configuredAgentId.trim() } : {}),
3384
3411
  cronExpr,
@@ -3396,7 +3423,7 @@ class AiHubServer {
3396
3423
  // GET /api/ai-hub/schedules — list scheduled deployments for one project.
3397
3424
  this.app.get('/api/ai-hub/schedules', (req, res) => {
3398
3425
  try {
3399
- const projectPath = deploymentProjectFilter(req.query.projectPath, this.projectPath);
3426
+ const projectPath = deploymentProjectFilter(req.query.projectPath, this.defaultProjectPath());
3400
3427
  return res.json(this.deploymentStore.load().filter((d) => d.type === 'scheduled' && deploymentBelongsToProject(d, projectPath, this.projectPath)));
3401
3428
  }
3402
3429
  catch (err) {
@@ -3430,7 +3457,7 @@ class AiHubServer {
3430
3457
  let resolvedProjectPath;
3431
3458
  try {
3432
3459
  if (projectPath !== undefined)
3433
- resolvedProjectPath = ensureDirectoryPath(projectPath || this.projectPath);
3460
+ resolvedProjectPath = ensureDirectoryPath(projectPath || this.defaultProjectPath());
3434
3461
  }
3435
3462
  catch (err) {
3436
3463
  return res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid project path.' });
@@ -3480,7 +3507,7 @@ class AiHubServer {
3480
3507
  type: 'webhook',
3481
3508
  label,
3482
3509
  jobId,
3483
- projectPath: ensureDirectoryPath(projectPath || this.projectPath),
3510
+ projectPath: ensureDirectoryPath(projectPath || this.defaultProjectPath()),
3484
3511
  hostId: resolvedHostId,
3485
3512
  ...(typeof configuredAgentId === 'string' && configuredAgentId.trim() ? { configuredAgentId: configuredAgentId.trim() } : {}),
3486
3513
  instructions: typeof instructions === 'string' ? instructions : undefined,
@@ -3497,7 +3524,7 @@ class AiHubServer {
3497
3524
  this.app.get('/api/ai-hub/webhooks', (req, res) => {
3498
3525
  const hubBase = this.hubBase;
3499
3526
  try {
3500
- const projectPath = deploymentProjectFilter(req.query.projectPath, this.projectPath);
3527
+ const projectPath = deploymentProjectFilter(req.query.projectPath, this.defaultProjectPath());
3501
3528
  return res.json(this.deploymentStore.load()
3502
3529
  .filter((d) => d.type === 'webhook' && deploymentBelongsToProject(d, projectPath, this.projectPath))
3503
3530
  .map((d) => ({ ...d, inboundUrl: `${hubBase}/api/ai-hub/webhooks/${d.id}/inbound` })));
@@ -3520,7 +3547,7 @@ class AiHubServer {
3520
3547
  let resolvedProjectPath;
3521
3548
  try {
3522
3549
  if (projectPath !== undefined)
3523
- resolvedProjectPath = ensureDirectoryPath(projectPath || this.projectPath);
3550
+ resolvedProjectPath = ensureDirectoryPath(projectPath || this.defaultProjectPath());
3524
3551
  }
3525
3552
  catch (err) {
3526
3553
  return res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid project path.' });
@@ -3623,7 +3650,7 @@ class AiHubServer {
3623
3650
  if (!jobName) {
3624
3651
  return res.status(400).json({ error: 'jobName is required.' });
3625
3652
  }
3626
- const projectPath = ensureDirectoryPath(reqProjectPath || this.projectPath);
3653
+ const projectPath = ensureDirectoryPath(reqProjectPath || this.defaultProjectPath());
3627
3654
  // Use the requested agent directly — caller specifies which agent to run (claude, codex, gemini).
3628
3655
  const requestedAgentId = req.body.configuredAgentId || employeeId;
3629
3656
  const requestedHostId = VALID_EMPLOYEE_IDS.includes(employeeId) ? employeeId : undefined;
@@ -3657,7 +3684,7 @@ class AiHubServer {
3657
3684
  phaseHistory: [],
3658
3685
  totals: emptyTotals(),
3659
3686
  lastStatusChangeAt: startTimestamp,
3660
- personaKey: getProtectedPersonaForHubJob(jobName),
3687
+ personaKey: getHubPersonaForJob(jobName),
3661
3688
  };
3662
3689
  // Register the run before spawning so onEvent/onExit callbacks can
3663
3690
  // safely call update() even if they fire synchronously (FakeHostRuntime).
@@ -3775,7 +3802,7 @@ class AiHubServer {
3775
3802
  phaseHistory: [],
3776
3803
  totals: emptyTotals(),
3777
3804
  lastStatusChangeAt: startTimestamp,
3778
- personaKey: jobMetadata?.personaKey ?? getProtectedPersonaForHubJob(jobId),
3805
+ personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
3779
3806
  };
3780
3807
  // Pre-register before startRun so synchronous onEvent calls (e.g. FakeHostRuntime)
3781
3808
  // can call runRegistry.update without "Run not found" throws.
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Sideloads the Word add-in manifest so it appears in Word's Developer Add-ins
4
+ * list without admin rights or AppSource publishing.
5
+ *
6
+ * Windows: writes a registry value under HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer
7
+ * macOS: writes an entry to ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/
8
+ *
9
+ * Both paths are non-admin and survive app updates (keyed by manifest GUID).
10
+ * Safe to call multiple times — checks before writing.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.isSideloaded = isSideloaded;
17
+ exports.sideloadManifest = sideloadManifest;
18
+ exports.removeSideload = removeSideload;
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const os_1 = __importDefault(require("os"));
22
+ const child_process_1 = require("child_process");
23
+ const MANIFEST_GUID = 'd1090951-50cf-4cf2-9d12-b0f8541d265c';
24
+ function resolveManifestPath(projectPath) {
25
+ const candidates = [
26
+ path_1.default.resolve(projectPath, 'extensions/office-word/manifest.xml'),
27
+ path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/manifest.xml'),
28
+ path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/manifest.xml'),
29
+ ];
30
+ return candidates.find(c => fs_1.default.existsSync(c)) ?? null;
31
+ }
32
+ function isSideloaded() {
33
+ if (process.platform === 'win32') {
34
+ const result = (0, child_process_1.spawnSync)('reg', [
35
+ 'query',
36
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
37
+ '/v', MANIFEST_GUID,
38
+ ], { encoding: 'utf8' });
39
+ return result.status === 0 && result.stdout.includes(MANIFEST_GUID);
40
+ }
41
+ if (process.platform === 'darwin') {
42
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
43
+ return fs_1.default.existsSync(path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`));
44
+ }
45
+ return false;
46
+ }
47
+ function sideloadManifest(projectPath) {
48
+ const manifestPath = resolveManifestPath(projectPath);
49
+ if (!manifestPath) {
50
+ return { ok: false, reason: 'Manifest file not found — is extensions/office-word/ present?' };
51
+ }
52
+ if (process.platform === 'win32') {
53
+ const result = (0, child_process_1.spawnSync)('reg', [
54
+ 'add',
55
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
56
+ '/v', MANIFEST_GUID,
57
+ '/t', 'REG_SZ',
58
+ '/d', manifestPath,
59
+ '/f',
60
+ ], { encoding: 'utf8' });
61
+ if (result.status !== 0) {
62
+ return { ok: false, reason: result.stderr || 'reg add failed' };
63
+ }
64
+ return { ok: true };
65
+ }
66
+ if (process.platform === 'darwin') {
67
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
68
+ try {
69
+ fs_1.default.mkdirSync(wefDir, { recursive: true });
70
+ fs_1.default.copyFileSync(manifestPath, path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`));
71
+ return { ok: true };
72
+ }
73
+ catch (err) {
74
+ return { ok: false, reason: String(err) };
75
+ }
76
+ }
77
+ return { ok: false, reason: `Unsupported platform: ${process.platform}` };
78
+ }
79
+ function removeSideload() {
80
+ if (process.platform === 'win32') {
81
+ (0, child_process_1.spawnSync)('reg', [
82
+ 'delete',
83
+ `HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer`,
84
+ '/v', MANIFEST_GUID,
85
+ '/f',
86
+ ], { encoding: 'utf8' });
87
+ return;
88
+ }
89
+ if (process.platform === 'darwin') {
90
+ const wefDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', 'com.microsoft.Word', 'Data', 'Documents', 'wef');
91
+ const target = path_1.default.join(wefDir, `${MANIFEST_GUID}.xml`);
92
+ if (fs_1.default.existsSync(target))
93
+ fs_1.default.unlinkSync(target);
94
+ }
95
+ }
@@ -33,7 +33,7 @@ function createFraimHubProgram(action = cli_1.runHub) {
33
33
  .description('Start the FRAIM Hub local companion')
34
34
  .version(readPackageVersion())
35
35
  .option('--port <port>', 'Preferred local port for the hub', (value) => Number(value), 43091)
36
- .option('--project-path <path>', 'Initial project path for job discovery', process.cwd())
36
+ .option('--project-path <path>', 'Initial project path for job discovery')
37
37
  .option('--no-open', 'Do not open the hub after startup')
38
38
  .option('--browser', 'Open in the default browser instead of the desktop shell')
39
39
  .option('--restart', 'Replace any running Hub instance with this build (even at the same version)')
@@ -96,18 +96,18 @@ ${buildFraimInvocationBody('generic-tool-discovery')}
96
96
  `;
97
97
  }
98
98
  function buildCodexSkillContent() {
99
- return `# FRAIM
100
-
99
+ return `# FRAIM
100
+
101
101
  ${buildFraimInvocationBody('codex-tool-search')}`;
102
102
  }
103
103
  function buildGrokSkillContent() {
104
- return `# FRAIM
105
-
104
+ return `# FRAIM
105
+
106
106
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
107
107
  }
108
108
  function buildWindsurfCommandContent() {
109
- return `# FRAIM
110
-
109
+ return `# FRAIM
110
+
111
111
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
112
112
  }
113
113
  function buildKiroCommandContent() {
@@ -3,9 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>FRAIM Hub</title>
7
- <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
8
- <script src="config.js" type="text/javascript"></script>
6
+ <title>FRAIM Hub</title>
7
+ <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
8
+ <script src="config.js" type="text/javascript"></script>
9
9
  <style>
10
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
11
11
  html, body { height: 100%; overflow: hidden; }
@@ -19,9 +19,9 @@
19
19
  // Loading an HTTP iframe from an HTTPS page is mixed-content-blocked, so use the
20
20
  // same origin as the taskpane when running over HTTPS (the ssl-proxy forwards all
21
21
  // routes to the Hub). HTTP (isolated tests) keeps the direct Hub address.
22
- var HUB_ORIGIN = window.location.protocol === 'https:'
23
- ? window.location.origin
24
- : (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
22
+ var HUB_ORIGIN = window.location.protocol === 'https:'
23
+ ? window.location.origin
24
+ : (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
25
25
  var hubFrame = document.getElementById('hub');
26
26
  var pendingPush = null; // context queued before hub-ready fires
27
27
  var hubReady = false;
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.207",
3
+ "version": "2.0.208",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -89,7 +89,7 @@
89
89
  "dotenv": "^16.4.7",
90
90
  "electron": "^41.2.2",
91
91
  "express": "^5.2.1",
92
- "fraim": "2.0.207",
92
+ "fraim": "2.0.208",
93
93
  "mongodb": "^7.0.0",
94
94
  "node-cron": "4.2.1",
95
95
  "node-edge-tts": "^1.2.10",