fraim 2.0.160 → 2.0.161

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.
@@ -82,22 +82,23 @@ function ensureLoginItem() {
82
82
  // ---------------------------------------------------------------------------
83
83
  // Word manifest sideload (runs once on first launch)
84
84
  // ---------------------------------------------------------------------------
85
- function ensureWordSideload(projectPath) {
85
+ function ensureWordSideload(projectPath, httpPort) {
86
86
  // Flag version bump: bump this string when new manifests are added so all
87
87
  // users get re-sideloaded on their next launch.
88
- const FLAG_VERSION = 'v3-filepath';
88
+ const FLAG_VERSION = 'v4-dynamic-port';
89
+ const expectedFlag = `${FLAG_VERSION}:${httpPort}`;
89
90
  const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'word-sideloaded.flag');
90
- if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === FLAG_VERSION)
91
+ if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === expectedFlag)
91
92
  return;
92
- if (!(0, office_sideload_1.isSideloaded)()) {
93
- const result = (0, office_sideload_1.sideloadManifest)(projectPath);
93
+ if (httpPort > 0) {
94
+ const result = (0, office_sideload_1.sideloadManifest)(projectPath, { httpPort, userDataDir: electron_1.app.getPath('userData') });
94
95
  if (!result.ok) {
95
96
  console.warn('[fraim] Office sideload skipped:', result.reason);
96
97
  return; // don't write flag — retry next launch
97
98
  }
98
99
  }
99
100
  fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
100
- fs_1.default.writeFileSync(flagPath, FLAG_VERSION);
101
+ fs_1.default.writeFileSync(flagPath, expectedFlag);
101
102
  }
102
103
  // ---------------------------------------------------------------------------
103
104
  // Tray setup
@@ -217,10 +218,8 @@ function stopServerOnce() {
217
218
  // Launch
218
219
  // ---------------------------------------------------------------------------
219
220
  async function launchDesktopShell(options) {
220
- const [httpPort, httpsPort] = await Promise.all([
221
- (0, server_1.findAvailablePort)(options.preferredPort),
222
- (0, server_1.findAvailablePort)(43092),
223
- ]);
221
+ const httpPort = await (0, server_1.findAvailablePort)(options.preferredPort);
222
+ const httpsPort = await (0, server_1.findAvailablePortExcluding)(43092, new Set([httpPort]));
224
223
  // Generate (or load cached) self-signed cert for HTTPS.
225
224
  // Fast on subsequent launches (file read); ~200ms on first launch (key gen).
226
225
  const certBundle = await (0, cert_store_1.loadOrCreateCert)();
@@ -239,7 +238,7 @@ async function launchDesktopShell(options) {
239
238
  },
240
239
  });
241
240
  await server.start(httpPort);
242
- ensureWordSideload(options.projectPath);
241
+ ensureWordSideload(options.projectPath, httpPort);
243
242
  const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
244
243
  createTray(hubUrl);
245
244
  await createWindow(hubUrl);
@@ -22,6 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.manifestXmlForPort = manifestXmlForPort;
25
26
  exports.isSideloaded = isSideloaded;
26
27
  exports.sideloadManifest = sideloadManifest;
27
28
  exports.removeSideload = removeSideload;
@@ -56,6 +57,22 @@ function resolveManifestPath(entry, projectPath) {
56
57
  function macWefPath(container, guid) {
57
58
  return path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', container, 'Data', 'Documents', 'wef', `${guid}.xml`);
58
59
  }
60
+ function generatedManifestPath(entry, userDataDir) {
61
+ return path_1.default.join(userDataDir, 'office-manifests', entry.guid, 'manifest.xml');
62
+ }
63
+ function manifestXmlForPort(xml, httpPort) {
64
+ return xml.replace(/(https?:\/\/(?:localhost|127\.0\.0\.1)):43091/g, `$1:${httpPort}`);
65
+ }
66
+ function prepareManifestForSideload(entry, sourcePath, options) {
67
+ if (!options.httpPort)
68
+ return sourcePath;
69
+ const userDataDir = options.userDataDir || path_1.default.join(os_1.default.homedir(), '.fraim');
70
+ const dest = generatedManifestPath(entry, userDataDir);
71
+ const rendered = manifestXmlForPort(fs_1.default.readFileSync(sourcePath, 'utf8'), options.httpPort);
72
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
73
+ fs_1.default.writeFileSync(dest, rendered, { encoding: 'utf8' });
74
+ return dest;
75
+ }
59
76
  // ---------------------------------------------------------------------------
60
77
  // Windows registry helpers
61
78
  // ---------------------------------------------------------------------------
@@ -92,17 +109,18 @@ function isManifestSideloaded(m) {
92
109
  * Sideloads all manifests. Returns ok:true if every manifest was registered.
93
110
  * Returns ok:false with the first failure reason.
94
111
  */
95
- function sideloadManifest(projectPath) {
112
+ function sideloadManifest(projectPath, options = {}) {
96
113
  for (const entry of MANIFESTS) {
97
114
  const manifestPath = resolveManifestPath(entry, projectPath);
98
115
  if (!manifestPath) {
99
116
  return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/ and public/ai-hub/powerpoint-taskpane/` };
100
117
  }
118
+ const sideloadPath = prepareManifestForSideload(entry, manifestPath, options);
101
119
  if (process.platform === 'win32') {
102
120
  // Developer-key value = absolute file path to manifest.xml (NOT a URL).
103
121
  const r = (0, child_process_1.spawnSync)('reg', [
104
122
  'add', WEF_DEVELOPER_KEY,
105
- '/v', entry.guid, '/t', 'REG_SZ', '/d', manifestPath, '/f',
123
+ '/v', entry.guid, '/t', 'REG_SZ', '/d', sideloadPath, '/f',
106
124
  ], { encoding: 'utf8' });
107
125
  if (r.status !== 0)
108
126
  return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
@@ -112,7 +130,7 @@ function sideloadManifest(projectPath) {
112
130
  const dest = macWefPath(entry.macContainer, entry.guid);
113
131
  try {
114
132
  fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
115
- fs_1.default.copyFileSync(manifestPath, dest);
133
+ fs_1.default.copyFileSync(sideloadPath, dest);
116
134
  }
117
135
  catch (err) {
118
136
  return { ok: false, reason: String(err) };
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AiHubServer = void 0;
7
7
  exports.findAvailablePort = findAvailablePort;
8
+ exports.findAvailablePortExcluding = findAvailablePortExcluding;
8
9
  const express_1 = __importDefault(require("express"));
9
10
  const path_1 = __importDefault(require("path"));
10
11
  const fs_1 = __importDefault(require("fs"));
@@ -488,6 +489,13 @@ class AiHubServer {
488
489
  });
489
490
  });
490
491
  }
492
+ this.app.get(['/word-taskpane/config.js', '/powerpoint-taskpane/config.js'], (_req, res) => {
493
+ const port = this.httpPort || 43091;
494
+ const origin = `http://127.0.0.1:${port}`;
495
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
496
+ res.setHeader('Access-Control-Allow-Origin', '*');
497
+ res.end(`window.FRAIM_HUB_ORIGIN=${JSON.stringify(origin)};\n`);
498
+ });
491
499
  this.app.get('/health', (_req, res) => {
492
500
  res.json({ status: 'ok', service: 'fraim-ai-hub' });
493
501
  });
@@ -501,6 +509,7 @@ class AiHubServer {
501
509
  return this.app;
502
510
  }
503
511
  async start(port) {
512
+ this.httpPort = port;
504
513
  if (this.dbService) {
505
514
  try {
506
515
  await this.dbService.connect();
@@ -1233,8 +1242,15 @@ class AiHubServer {
1233
1242
  }
1234
1243
  exports.AiHubServer = AiHubServer;
1235
1244
  async function findAvailablePort(preferredPort) {
1245
+ return findAvailablePortExcluding(preferredPort, new Set());
1246
+ }
1247
+ async function findAvailablePortExcluding(preferredPort, excludedPorts) {
1236
1248
  let port = preferredPort;
1237
1249
  while (port < preferredPort + 20) {
1250
+ if (excludedPorts.has(port)) {
1251
+ port += 1;
1252
+ continue;
1253
+ }
1238
1254
  const available = await new Promise((resolve) => {
1239
1255
  const server = net_1.default.createServer();
1240
1256
  server.once('error', () => resolve(false));
@@ -267,6 +267,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
267
267
  ? [
268
268
  '```javascript',
269
269
  'evidence: {',
270
+ ' artifactPath: "<project-relative quality artifact path>",',
270
271
  ' quality: {',
271
272
  ' gateDecision: "<pass|flag|fail>",',
272
273
  ' interviewsAnalyzed: <number>,',
@@ -281,6 +282,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
281
282
  ? [
282
283
  '```javascript',
283
284
  'evidence: {',
285
+ ' artifactPath: "<project-relative quality artifact path>",',
284
286
  ' quality: {',
285
287
  ' composite: <number 0-10>,',
286
288
  ' participant: { fit: <number 1-10>, urgency: <number 1-10>, authority: <number 1-10> },',
@@ -298,6 +300,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
298
300
  : [
299
301
  '```javascript',
300
302
  'evidence: {',
303
+ ' artifactPath: "<project-relative quality artifact path>",',
301
304
  ' quality: {',
302
305
  ' composite: <number 0-10>,',
303
306
  ' coaching: "<actionable recommendation>",',
@@ -315,7 +318,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
315
318
  return [
316
319
  `❌ **Job completion rejected** for \`${jobName}\`.`,
317
320
  '',
318
- `This job is required to emit a quality score on its final \`seekMentoring\` call so the result is captured in \`fraim_quality_scores\` for the quality dashboard. The following problems were found in \`evidence.quality\`:`,
321
+ `This job is required to emit quality evidence on its final \`seekMentoring\` call so the result is captured in \`fraim_quality_scores\` for the quality dashboard. The following problems were found:`,
319
322
  '',
320
323
  errorBullets,
321
324
  ...importantNote,
@@ -663,14 +663,28 @@ class FraimLocalMCPServer {
663
663
  }
664
664
  async performLocalCatalogSync(projectRoot) {
665
665
  const { runSync } = await Promise.resolve().then(() => __importStar(require('../cli/commands/sync')));
666
- await runSync({
667
- projectRoot,
668
- skipUpdates: true,
669
- local: this.shouldUseLocalSyncTarget(),
670
- failHard: 'throw'
666
+ await this.runWithConsoleLogRedirectedToStderr(async () => {
667
+ await runSync({
668
+ projectRoot,
669
+ skipUpdates: true,
670
+ local: this.shouldUseLocalSyncTarget(),
671
+ failHard: 'throw'
672
+ });
671
673
  });
672
674
  this.writeLocalCatalogMetadata(projectRoot);
673
675
  }
676
+ async runWithConsoleLogRedirectedToStderr(operation) {
677
+ const originalLog = console.log;
678
+ console.log = (...args) => {
679
+ console.error(...args);
680
+ };
681
+ try {
682
+ return await operation();
683
+ }
684
+ finally {
685
+ console.log = originalLog;
686
+ }
687
+ }
674
688
  async ensureFreshLocalCatalogOnConnect(requestId) {
675
689
  if (process.env.FRAIM_DISABLE_SYNC_ON_CONNECT === '1') {
676
690
  this.latestConnectSyncWarning = null;
@@ -1863,7 +1877,7 @@ class FraimLocalMCPServer {
1863
1877
  //
1864
1878
  // When a QUALITY_PRODUCING_JOBS job reaches its final phase
1865
1879
  // (nextPhase === null) with status === 'complete', the agent MUST
1866
- // include a valid `evidence.quality` object. If invalid, swap the
1880
+ // include `evidence.artifactPath` and a valid `evidence.quality` object. If invalid, swap the
1867
1881
  // accomplishment message for a rejection and DO NOT emit. If valid,
1868
1882
  // fire-and-forget POST to /api/analytics/quality-score so the row
1869
1883
  // lands in fraim_quality_scores.
@@ -1871,8 +1885,13 @@ class FraimLocalMCPServer {
1871
1885
  const isFinalCompletion = args.status === 'complete' &&
1872
1886
  (tutoringResponse.nextPhase === null || tutoringResponse.nextPhase === undefined);
1873
1887
  if (isQualityJob && isFinalCompletion) {
1874
- const qualityErrors = (0, quality_evidence_1.validateQualityEvidence)(args.evidence?.quality, args.jobName);
1875
- if (qualityErrors) {
1888
+ const qualityErrors = [
1889
+ ...((0, quality_evidence_1.validateQualityEvidence)(args.evidence?.quality, args.jobName) || []),
1890
+ ...(typeof args.evidence?.artifactPath === 'string' && args.evidence.artifactPath.trim()
1891
+ ? []
1892
+ : ['evidence.artifactPath is required'])
1893
+ ];
1894
+ if (qualityErrors.length > 0) {
1876
1895
  this.log(`❌ Quality enforcement rejected ${args.jobName} completion: ${qualityErrors.join('; ')}`);
1877
1896
  const rejection = (0, quality_evidence_1.buildQualityRejectionMessage)(args.jobName, args.currentPhase, qualityErrors);
1878
1897
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
@@ -1886,7 +1905,7 @@ class FraimLocalMCPServer {
1886
1905
  jobId: args.jobId,
1887
1906
  sessionId: requestSessionId || args.sessionId || 'unknown',
1888
1907
  quality: args.evidence.quality,
1889
- artifactPath: args.evidence.quality.artifactPath,
1908
+ artifactPath: args.evidence?.artifactPath,
1890
1909
  repoIdentifier: args.evidence?.reviewContext?.repoIdentifier || this.repoInfo?.url,
1891
1910
  reviewContext: {
1892
1911
  ...(args.evidence?.reviewContext || {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.160",
3
+ "version": "2.0.161",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>FRAIM Hub</title>
7
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>
8
9
  <style>
9
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
10
11
  html, body { height: 100%; overflow: hidden; }
@@ -25,7 +26,7 @@
25
26
  // desktop (HTTP) use the direct Hub address.
26
27
  var HUB_ORIGIN = window.location.protocol === 'https:'
27
28
  ? window.location.origin
28
- : 'http://127.0.0.1:43091';
29
+ : (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
29
30
  var hubFrame = document.getElementById('hub');
30
31
  var pendingPush = null; // context queued before hub-ready fires
31
32
  var hubReady = false;