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.
- package/dist/src/ai-hub/desktop-main.js +10 -11
- package/dist/src/ai-hub/office-sideload.js +21 -3
- package/dist/src/ai-hub/server.js +16 -0
- package/dist/src/core/quality-evidence.js +4 -1
- package/dist/src/local-mcp-server/stdio-server.js +28 -9
- package/package.json +1 -1
- package/public/ai-hub/powerpoint-taskpane/index.html +2 -1
|
@@ -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 = '
|
|
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() ===
|
|
91
|
+
if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === expectedFlag)
|
|
91
92
|
return;
|
|
92
|
-
if (
|
|
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,
|
|
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
|
|
221
|
-
|
|
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',
|
|
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(
|
|
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
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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 =
|
|
1875
|
-
|
|
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
|
|
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
|
@@ -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;
|