fraim-framework 2.0.196 → 2.0.198
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/brand-store.js +232 -0
- package/dist/src/ai-hub/server.js +91 -1
- package/dist/src/cli/doctor/checks/fraim-mcp-diagnostics.js +283 -0
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +224 -18
- package/dist/src/cli/utils/org-pack-sync.js +35 -5
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -0
- package/package.json +1 -1
- package/public/ai-hub/index.html +17 -1
- package/public/ai-hub/script.js +441 -21
- package/public/ai-hub/styles.css +67 -0
- package/public/first-run/script.js +98 -25
- package/public/first-run/styles.css +34 -8
|
@@ -56,11 +56,51 @@ const toml = __importStar(require("toml"));
|
|
|
56
56
|
const ide_detector_1 = require("../../setup/ide-detector");
|
|
57
57
|
const fraim_mcp_latest_launcher_1 = require("../../mcp/fraim-mcp-latest-launcher");
|
|
58
58
|
const command_resolution_1 = require("../../mcp/command-resolution");
|
|
59
|
+
const fraim_mcp_diagnostics_1 = require("./fraim-mcp-diagnostics");
|
|
59
60
|
// Cache the npm major version so execFileSync is called at most once per process.
|
|
60
61
|
// Without caching, each IDE config check calls diagnoseFraimMcpLaunchPlan which calls
|
|
61
62
|
// getNpmMajorVersion, and execFileSync blocks the event loop for ~2s on Windows.
|
|
62
63
|
// With 8+ IDE checks running via Promise.all the blocking stacks sequentially.
|
|
63
64
|
let _npmMajorVersionCache = undefined;
|
|
65
|
+
function getGlobalFraimConfigPath() {
|
|
66
|
+
return path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
|
|
67
|
+
}
|
|
68
|
+
function resolveFraimApiKeyForDiagnostics() {
|
|
69
|
+
const configPath = getGlobalFraimConfigPath();
|
|
70
|
+
if (process.env.FRAIM_API_KEY) {
|
|
71
|
+
return {
|
|
72
|
+
present: true,
|
|
73
|
+
source: 'env',
|
|
74
|
+
configPath,
|
|
75
|
+
apiKey: process.env.FRAIM_API_KEY,
|
|
76
|
+
fingerprint: (0, fraim_mcp_diagnostics_1.fingerprintSecret)(process.env.FRAIM_API_KEY)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
81
|
+
return { present: false, source: 'missing', configPath };
|
|
82
|
+
}
|
|
83
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
84
|
+
if (config.apiKey) {
|
|
85
|
+
return {
|
|
86
|
+
present: true,
|
|
87
|
+
source: 'global-config',
|
|
88
|
+
configPath,
|
|
89
|
+
apiKey: String(config.apiKey),
|
|
90
|
+
fingerprint: (0, fraim_mcp_diagnostics_1.fingerprintSecret)(String(config.apiKey))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { present: false, source: 'missing', configPath };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
present: false,
|
|
98
|
+
source: 'config-error',
|
|
99
|
+
configPath,
|
|
100
|
+
error: error.message
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
64
104
|
function getNpmMajorVersion() {
|
|
65
105
|
if (_npmMajorVersionCache !== undefined) {
|
|
66
106
|
return _npmMajorVersionCache;
|
|
@@ -130,7 +170,7 @@ async function testFraimConnectivity() {
|
|
|
130
170
|
}
|
|
131
171
|
};
|
|
132
172
|
}
|
|
133
|
-
const globalConfigPath =
|
|
173
|
+
const globalConfigPath = getGlobalFraimConfigPath();
|
|
134
174
|
let apiKey;
|
|
135
175
|
try {
|
|
136
176
|
const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
|
|
@@ -164,7 +204,7 @@ async function testFraimConnectivity() {
|
|
|
164
204
|
}
|
|
165
205
|
// Test connectivity by making a request to the FRAIM server
|
|
166
206
|
const startTime = Date.now();
|
|
167
|
-
const remoteUrl = process.env.FRAIM_REMOTE_URL ||
|
|
207
|
+
const remoteUrl = process.env.FRAIM_REMOTE_URL || fraim_mcp_diagnostics_1.DEFAULT_FRAIM_REMOTE_URL;
|
|
168
208
|
// Debug: Log what we're testing (helpful for troubleshooting)
|
|
169
209
|
if (process.env.DEBUG_DOCTOR) {
|
|
170
210
|
console.log('[DOCTOR DEBUG] Testing FRAIM connectivity:');
|
|
@@ -401,6 +441,24 @@ async function validateIDEMCPConfig(ide) {
|
|
|
401
441
|
}
|
|
402
442
|
};
|
|
403
443
|
}
|
|
444
|
+
if (fraimServer.command) {
|
|
445
|
+
const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
|
|
446
|
+
const consistencyDiagnostic = (0, fraim_mcp_diagnostics_1.diagnoseFraimServerConfigConsistency)(fraimServer, {
|
|
447
|
+
expectedApiKey: keyDiagnostic.apiKey,
|
|
448
|
+
expectedRemoteUrl: process.env.FRAIM_REMOTE_URL || fraim_mcp_diagnostics_1.DEFAULT_FRAIM_REMOTE_URL
|
|
449
|
+
});
|
|
450
|
+
if (consistencyDiagnostic.status !== 'passed') {
|
|
451
|
+
return {
|
|
452
|
+
...consistencyDiagnostic,
|
|
453
|
+
details: {
|
|
454
|
+
...consistencyDiagnostic.details,
|
|
455
|
+
configPath,
|
|
456
|
+
keySource: keyDiagnostic.source,
|
|
457
|
+
keyConfigPath: keyDiagnostic.configPath
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
404
462
|
// Check for auth header (for HTTP servers)
|
|
405
463
|
if (fraimServer.url) {
|
|
406
464
|
const hasAuth = fraimServer.headers?.Authorization || fraimServer.http_headers?.Authorization;
|
|
@@ -432,6 +490,84 @@ async function validateIDEMCPConfig(ide) {
|
|
|
432
490
|
};
|
|
433
491
|
}
|
|
434
492
|
}
|
|
493
|
+
async function testFraimLocalProxyTroubleshooting() {
|
|
494
|
+
if (process.env.NODE_ENV === 'test') {
|
|
495
|
+
return {
|
|
496
|
+
status: 'warning',
|
|
497
|
+
message: 'FRAIM local proxy troubleshooting skipped (test mode)',
|
|
498
|
+
details: {
|
|
499
|
+
testMode: true,
|
|
500
|
+
skipped: true,
|
|
501
|
+
diagnosticVersion: 'fraim-mcp-startup-v1'
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
|
|
506
|
+
const logPath = path_1.default.join(os_1.default.tmpdir(), fraim_mcp_diagnostics_1.FRAIM_PROXY_LOG_FILE);
|
|
507
|
+
if (!keyDiagnostic.present || !keyDiagnostic.apiKey) {
|
|
508
|
+
return {
|
|
509
|
+
status: 'error',
|
|
510
|
+
message: 'FRAIM local proxy troubleshooting cannot resolve an API key',
|
|
511
|
+
suggestion: keyDiagnostic.source === 'config-error'
|
|
512
|
+
? `Fix JSON syntax in ${keyDiagnostic.configPath}`
|
|
513
|
+
: 'Run: fraim setup or set FRAIM_API_KEY before running MCP diagnostics',
|
|
514
|
+
details: {
|
|
515
|
+
diagnosticVersion: 'fraim-mcp-startup-v1',
|
|
516
|
+
keySource: keyDiagnostic.source,
|
|
517
|
+
keyConfigPath: keyDiagnostic.configPath,
|
|
518
|
+
keyError: keyDiagnostic.error
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (!fs_1.default.existsSync(logPath)) {
|
|
523
|
+
const classification = 'no-log-file';
|
|
524
|
+
return {
|
|
525
|
+
status: 'warning',
|
|
526
|
+
message: 'FRAIM local proxy log not found',
|
|
527
|
+
suggestion: (0, fraim_mcp_diagnostics_1.buildAgentDebugSteps)(classification)[0],
|
|
528
|
+
details: {
|
|
529
|
+
diagnosticVersion: 'fraim-mcp-startup-v1',
|
|
530
|
+
classification,
|
|
531
|
+
interpretation: (0, fraim_mcp_diagnostics_1.interpretationForClassification)(classification),
|
|
532
|
+
agentDebugSteps: (0, fraim_mcp_diagnostics_1.buildAgentDebugSteps)(classification),
|
|
533
|
+
logPath,
|
|
534
|
+
keySource: keyDiagnostic.source,
|
|
535
|
+
keyFingerprint: keyDiagnostic.fingerprint
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const logTail = (0, fraim_mcp_diagnostics_1.readFileTailUtf8)(logPath);
|
|
540
|
+
const analysis = (0, fraim_mcp_diagnostics_1.analyzeFraimProxyLog)(logTail.content, {
|
|
541
|
+
expectedKey: keyDiagnostic.apiKey,
|
|
542
|
+
maxLines: 300
|
|
543
|
+
});
|
|
544
|
+
const errorClassifications = [
|
|
545
|
+
'message-processing-failed',
|
|
546
|
+
'remote-request-failed'
|
|
547
|
+
];
|
|
548
|
+
const passedClassifications = [
|
|
549
|
+
'local-proxy-forwarded-to-remote'
|
|
550
|
+
];
|
|
551
|
+
const status = errorClassifications.includes(analysis.classification)
|
|
552
|
+
? 'error'
|
|
553
|
+
: passedClassifications.includes(analysis.classification)
|
|
554
|
+
? 'passed'
|
|
555
|
+
: 'warning';
|
|
556
|
+
return {
|
|
557
|
+
status,
|
|
558
|
+
message: `FRAIM local proxy diagnostic: ${analysis.interpretation}`,
|
|
559
|
+
suggestion: status === 'passed' ? undefined : analysis.agentDebugSteps[0],
|
|
560
|
+
details: {
|
|
561
|
+
diagnosticVersion: 'fraim-mcp-startup-v1',
|
|
562
|
+
logPath,
|
|
563
|
+
logBytesRead: logTail.bytesRead,
|
|
564
|
+
logFileSize: logTail.fileSize,
|
|
565
|
+
keySource: keyDiagnostic.source,
|
|
566
|
+
keyFingerprint: keyDiagnostic.fingerprint,
|
|
567
|
+
...analysis
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
435
571
|
// ============================================================
|
|
436
572
|
// Issue #532: Stdio MCP Runtime Connectivity Check
|
|
437
573
|
// ============================================================
|
|
@@ -447,6 +583,8 @@ const STDIO_HANDSHAKE_TIMEOUT_MS = 15000;
|
|
|
447
583
|
async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIMEOUT_MS) {
|
|
448
584
|
return new Promise((resolve) => {
|
|
449
585
|
const startTime = Date.now();
|
|
586
|
+
const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
|
|
587
|
+
const knownSecrets = keyDiagnostic.apiKey ? [keyDiagnostic.apiKey] : [];
|
|
450
588
|
let spawnCommand;
|
|
451
589
|
let spawnArgs;
|
|
452
590
|
// spawnOptions is typed loosely so we can set windowsVerbatimArguments below.
|
|
@@ -492,14 +630,34 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
492
630
|
});
|
|
493
631
|
return;
|
|
494
632
|
}
|
|
633
|
+
let settled = false;
|
|
634
|
+
let initializeResponse = null;
|
|
635
|
+
let toolsListResponse = null;
|
|
636
|
+
const settle = (result, killChild = false) => {
|
|
637
|
+
if (settled)
|
|
638
|
+
return;
|
|
639
|
+
settled = true;
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
if (killChild) {
|
|
642
|
+
try {
|
|
643
|
+
child.stdin?.end();
|
|
644
|
+
}
|
|
645
|
+
catch { /* best effort */ }
|
|
646
|
+
try {
|
|
647
|
+
child.kill('SIGTERM');
|
|
648
|
+
}
|
|
649
|
+
catch { /* best effort */ }
|
|
650
|
+
}
|
|
651
|
+
resolve(result);
|
|
652
|
+
};
|
|
495
653
|
const timer = setTimeout(() => {
|
|
496
654
|
try {
|
|
497
655
|
child.kill('SIGKILL');
|
|
498
656
|
}
|
|
499
657
|
catch { /* best effort */ }
|
|
500
|
-
|
|
658
|
+
settle({
|
|
501
659
|
status: 'error',
|
|
502
|
-
phase: 'initialize',
|
|
660
|
+
phase: initializeResponse ? 'tools-list' : 'initialize',
|
|
503
661
|
message: `MCP server did not respond within ${timeoutMs}ms`,
|
|
504
662
|
suggestion: 'The server may be slow to start. Check that the package is installed and npx cache is warm.',
|
|
505
663
|
details: { command, args, timeoutMs, elapsed: Date.now() - startTime }
|
|
@@ -507,46 +665,89 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
507
665
|
}, timeoutMs);
|
|
508
666
|
let stderrBuffer = '';
|
|
509
667
|
let stdoutBuffer = '';
|
|
668
|
+
let stdoutLineBuffer = '';
|
|
510
669
|
let spawnError = null;
|
|
511
670
|
child.stderr?.on('data', (d) => {
|
|
512
|
-
stderrBuffer
|
|
671
|
+
stderrBuffer = (0, fraim_mcp_diagnostics_1.appendLimited)(stderrBuffer, d.toString());
|
|
513
672
|
});
|
|
514
673
|
child.on('error', (err) => {
|
|
515
674
|
spawnError = err;
|
|
516
675
|
});
|
|
676
|
+
const handleStdoutLine = (line) => {
|
|
677
|
+
const trimmed = line.trim();
|
|
678
|
+
if (!trimmed)
|
|
679
|
+
return;
|
|
680
|
+
try {
|
|
681
|
+
const msg = JSON.parse(trimmed);
|
|
682
|
+
if (msg?.id === 1 && msg?.result !== undefined) {
|
|
683
|
+
initializeResponse = msg;
|
|
684
|
+
}
|
|
685
|
+
if (msg?.id === 2 && msg?.result !== undefined) {
|
|
686
|
+
toolsListResponse = msg;
|
|
687
|
+
}
|
|
688
|
+
if (initializeResponse && toolsListResponse) {
|
|
689
|
+
const toolCount = Array.isArray(toolsListResponse.result?.tools)
|
|
690
|
+
? toolsListResponse.result.tools.length
|
|
691
|
+
: 0;
|
|
692
|
+
const elapsed = Date.now() - startTime;
|
|
693
|
+
settle({
|
|
694
|
+
status: 'passed',
|
|
695
|
+
message: `MCP server responded with ${toolCount} tool(s) in ${elapsed}ms`,
|
|
696
|
+
details: { command, args, toolCount, elapsed }
|
|
697
|
+
}, true);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// Ignore non-JSON lines (e.g., startup log output)
|
|
702
|
+
}
|
|
703
|
+
};
|
|
517
704
|
child.stdout?.on('data', (d) => {
|
|
518
|
-
|
|
705
|
+
const chunk = d.toString();
|
|
706
|
+
stdoutBuffer = (0, fraim_mcp_diagnostics_1.appendLimited)(stdoutBuffer, chunk);
|
|
707
|
+
stdoutLineBuffer += chunk;
|
|
708
|
+
let newlineIndex;
|
|
709
|
+
while ((newlineIndex = stdoutLineBuffer.indexOf('\n')) !== -1) {
|
|
710
|
+
const line = stdoutLineBuffer.slice(0, newlineIndex);
|
|
711
|
+
stdoutLineBuffer = stdoutLineBuffer.slice(newlineIndex + 1);
|
|
712
|
+
handleStdoutLine(line);
|
|
713
|
+
}
|
|
519
714
|
});
|
|
520
715
|
child.on('close', (code) => {
|
|
716
|
+
if (settled)
|
|
717
|
+
return;
|
|
521
718
|
clearTimeout(timer);
|
|
522
719
|
if (spawnError) {
|
|
523
720
|
const msg = spawnError.message || '';
|
|
524
721
|
const isEnoent = msg.includes('ENOENT') || msg.includes('not found');
|
|
525
|
-
|
|
722
|
+
settle({
|
|
526
723
|
status: 'error',
|
|
527
724
|
phase: 'spawn',
|
|
528
725
|
message: `Failed to start MCP server: ${msg}`,
|
|
529
726
|
suggestion: isEnoent
|
|
530
727
|
? 'Verify that npx is installed. Run: npm install -g npm to update npm/npx.'
|
|
531
728
|
: 'Check that the required package can be installed via npx.',
|
|
532
|
-
details: { command, args, error: msg }
|
|
729
|
+
details: { command, args, error: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(msg, knownSecrets) }
|
|
533
730
|
});
|
|
534
731
|
return;
|
|
535
732
|
}
|
|
536
733
|
if (code !== 0 && stdoutBuffer.trim() === '') {
|
|
537
|
-
|
|
734
|
+
settle({
|
|
538
735
|
status: 'error',
|
|
539
736
|
phase: 'spawn',
|
|
540
737
|
message: `MCP server exited with code ${code} before responding`,
|
|
541
738
|
suggestion: 'Run: npx -y <package> manually to check for installation errors.',
|
|
542
|
-
details: { command, args, exitCode: code, stderr: stderrBuffer.slice(
|
|
739
|
+
details: { command, args, exitCode: code, stderr: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(stderrBuffer.slice(-1000), knownSecrets) }
|
|
543
740
|
});
|
|
544
741
|
return;
|
|
545
742
|
}
|
|
546
|
-
|
|
743
|
+
if (stdoutLineBuffer.trim()) {
|
|
744
|
+
handleStdoutLine(stdoutLineBuffer);
|
|
745
|
+
}
|
|
746
|
+
if (settled)
|
|
747
|
+
return;
|
|
748
|
+
// Parse all JSON-RPC messages received from stdout as a fallback for servers
|
|
749
|
+
// that closed without a trailing newline.
|
|
547
750
|
const lines = stdoutBuffer.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
548
|
-
let initializeResponse = null;
|
|
549
|
-
let toolsListResponse = null;
|
|
550
751
|
for (const line of lines) {
|
|
551
752
|
try {
|
|
552
753
|
const msg = JSON.parse(line);
|
|
@@ -562,17 +763,17 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
562
763
|
}
|
|
563
764
|
}
|
|
564
765
|
if (!initializeResponse) {
|
|
565
|
-
|
|
766
|
+
settle({
|
|
566
767
|
status: 'error',
|
|
567
768
|
phase: 'initialize',
|
|
568
769
|
message: 'MCP server did not return a valid initialize response',
|
|
569
770
|
suggestion: 'The server may have crashed during startup. Check stderr for error details.',
|
|
570
|
-
details: { command, args, exitCode: code, stderr: stderrBuffer.slice(
|
|
771
|
+
details: { command, args, exitCode: code, stderr: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(stderrBuffer.slice(-1000), knownSecrets) }
|
|
571
772
|
});
|
|
572
773
|
return;
|
|
573
774
|
}
|
|
574
775
|
if (!toolsListResponse) {
|
|
575
|
-
|
|
776
|
+
settle({
|
|
576
777
|
status: 'error',
|
|
577
778
|
phase: 'tools-list',
|
|
578
779
|
message: 'MCP server did not respond to tools/list',
|
|
@@ -585,7 +786,7 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
585
786
|
? toolsListResponse.result.tools.length
|
|
586
787
|
: 0;
|
|
587
788
|
const elapsed = Date.now() - startTime;
|
|
588
|
-
|
|
789
|
+
settle({
|
|
589
790
|
status: 'passed',
|
|
590
791
|
message: `MCP server responded with ${toolCount} tool(s) in ${elapsed}ms`,
|
|
591
792
|
details: { command, args, toolCount, elapsed }
|
|
@@ -611,7 +812,6 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
611
812
|
params: {}
|
|
612
813
|
}) + '\n';
|
|
613
814
|
child.stdin?.write(toolsMsg);
|
|
614
|
-
child.stdin?.end();
|
|
615
815
|
}
|
|
616
816
|
catch {
|
|
617
817
|
// stdin may not be writable if the process exited immediately
|
|
@@ -686,6 +886,12 @@ function getMCPConnectivityChecks() {
|
|
|
686
886
|
critical: true,
|
|
687
887
|
run: testFraimConnectivity
|
|
688
888
|
});
|
|
889
|
+
checks.push({
|
|
890
|
+
name: 'FRAIM local proxy troubleshooting',
|
|
891
|
+
category: 'mcpConnectivity',
|
|
892
|
+
critical: false,
|
|
893
|
+
run: testFraimLocalProxyTroubleshooting
|
|
894
|
+
});
|
|
689
895
|
// Check 2: Validate each installed IDE's MCP config
|
|
690
896
|
const installedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
691
897
|
for (const ide of installedIDEs) {
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = void 0;
|
|
7
|
+
exports.isSafePackPath = isSafePackPath;
|
|
8
|
+
exports.shouldDecorateAsMarkdown = shouldDecorateAsMarkdown;
|
|
7
9
|
exports.getOrgCacheDir = getOrgCacheDir;
|
|
8
10
|
exports.readOrgCacheMetadata = readOrgCacheMetadata;
|
|
9
11
|
exports.getOrgCacheAgeHours = getOrgCacheAgeHours;
|
|
@@ -37,6 +39,28 @@ const ORG_PACK_DIRS = ['context', 'rules', 'learnings'];
|
|
|
37
39
|
* backend response can never write outside the cache directory.
|
|
38
40
|
*/
|
|
39
41
|
const SAFE_PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
|
|
42
|
+
/**
|
|
43
|
+
* Issue #744: the org brand descriptor is stored ALONGSIDE company info, in the
|
|
44
|
+
* same org context scope as org_context.md, as a single JSON file. Allow exactly
|
|
45
|
+
* that path (not arbitrary JSON) so cobranding syncs like org context.
|
|
46
|
+
*/
|
|
47
|
+
const ORG_BRAND_PACK_PATH = 'context/org_brand.json';
|
|
48
|
+
/**
|
|
49
|
+
* True when a pack-relative path is safe to materialize: a markdown file inside
|
|
50
|
+
* one of the three org pack dirs, or the org brand descriptor. A single guard
|
|
51
|
+
* shared by the git and cloud paths.
|
|
52
|
+
*/
|
|
53
|
+
function isSafePackPath(relativePath) {
|
|
54
|
+
return SAFE_PACK_RELATIVE_PATH.test(relativePath) || relativePath === ORG_BRAND_PACK_PATH;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Managed-content decoration prepends a markdown "synced from your org" header.
|
|
58
|
+
* It must only touch markdown — prepending it to org_brand.json would corrupt
|
|
59
|
+
* the JSON (#744).
|
|
60
|
+
*/
|
|
61
|
+
function shouldDecorateAsMarkdown(relativePath) {
|
|
62
|
+
return relativePath.toLowerCase().endsWith('.md');
|
|
63
|
+
}
|
|
40
64
|
function getOrgCacheDir() {
|
|
41
65
|
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_CACHE_DIRNAME);
|
|
42
66
|
}
|
|
@@ -83,10 +107,12 @@ function collectGitPackFiles(snapshotDir) {
|
|
|
83
107
|
if (!fs_1.default.existsSync(dirPath))
|
|
84
108
|
continue;
|
|
85
109
|
for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
|
|
86
|
-
|
|
110
|
+
const relativePath = `${dirName}/${entry.name}`;
|
|
111
|
+
// Markdown in any pack dir, plus the org brand descriptor (#744).
|
|
112
|
+
if (!entry.isFile() || !isSafePackPath(relativePath))
|
|
87
113
|
continue;
|
|
88
114
|
files.push({
|
|
89
|
-
relativePath
|
|
115
|
+
relativePath,
|
|
90
116
|
content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
|
|
91
117
|
});
|
|
92
118
|
}
|
|
@@ -101,7 +127,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
|
|
|
101
127
|
const files = Array.isArray(response.data?.files) ? response.data.files : [];
|
|
102
128
|
return {
|
|
103
129
|
files: files.filter((f) => typeof f?.relativePath === 'string' &&
|
|
104
|
-
|
|
130
|
+
isSafePackPath(f.relativePath) &&
|
|
105
131
|
typeof f?.content === 'string'),
|
|
106
132
|
version: String(response.data?.version ?? '0')
|
|
107
133
|
};
|
|
@@ -113,11 +139,15 @@ function materializeCache(files, metadata) {
|
|
|
113
139
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
114
140
|
fs_1.default.mkdirSync(stagingDir, { recursive: true });
|
|
115
141
|
for (const file of files) {
|
|
116
|
-
if (!
|
|
142
|
+
if (!isSafePackPath(file.relativePath))
|
|
117
143
|
continue;
|
|
118
144
|
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
119
145
|
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
120
|
-
|
|
146
|
+
// #744: only markdown gets the managed-content header; JSON is written verbatim.
|
|
147
|
+
const content = shouldDecorateAsMarkdown(file.relativePath)
|
|
148
|
+
? decorateManagedOrgFile(file.content, metadata.backend)
|
|
149
|
+
: file.content;
|
|
150
|
+
fs_1.default.writeFileSync(destination, content);
|
|
121
151
|
}
|
|
122
152
|
fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.ORG_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
|
|
123
153
|
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
@@ -14,6 +14,8 @@ exports.buildTeamContextSection = buildTeamContextSection;
|
|
|
14
14
|
exports.resolveTeamContextFiles = resolveTeamContextFiles;
|
|
15
15
|
exports.isTeamContextKey = isTeamContextKey;
|
|
16
16
|
exports.resolveTeamContextFile = resolveTeamContextFile;
|
|
17
|
+
exports.readOrgBrand = readOrgBrand;
|
|
18
|
+
exports.resolveOrgBrandWriteDir = resolveOrgBrandWriteDir;
|
|
17
19
|
exports.countPreservedLearnings = countPreservedLearnings;
|
|
18
20
|
exports.readPreservedLearnings = readPreservedLearnings;
|
|
19
21
|
exports.applyLearningEntryChange = applyLearningEntryChange;
|
|
@@ -21,6 +23,7 @@ exports.isTruthyFlag = isTruthyFlag;
|
|
|
21
23
|
const fs_1 = require("fs");
|
|
22
24
|
const path_1 = require("path");
|
|
23
25
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
26
|
+
const brand_store_1 = require("../ai-hub/brand-store");
|
|
24
27
|
const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
|
|
25
28
|
const DEFAULT_THRESHOLD = 3.0;
|
|
26
29
|
const AGING_HORIZON_DAYS = 7;
|
|
@@ -695,6 +698,44 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
695
698
|
scope
|
|
696
699
|
};
|
|
697
700
|
}
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Issue #744 — Org brand (Hub cobranding).
|
|
703
|
+
//
|
|
704
|
+
// The brand descriptor (company name/logo/color) is stored ALONGSIDE company
|
|
705
|
+
// info, in the SAME org context storage as org_context.md (manager directive,
|
|
706
|
+
// spec R7.4), as `context/org_brand.json`. Read resolves with the same org-scope
|
|
707
|
+
// layering as org context (repo-local override → synced org cache → user-level);
|
|
708
|
+
// writes target the same writable location org context edits use.
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
/** Ordered directories to look for org_brand.json, mirroring org-context layering. */
|
|
711
|
+
function orgBrandCandidateDirs(workspaceRoot) {
|
|
712
|
+
return [
|
|
713
|
+
(0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', 'context'),
|
|
714
|
+
(0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', 'context'),
|
|
715
|
+
(0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'context'),
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
/** Read the resolved org brand, or null when none is set on this machine. */
|
|
719
|
+
function readOrgBrand(workspaceRoot) {
|
|
720
|
+
for (const dir of orgBrandCandidateDirs(workspaceRoot)) {
|
|
721
|
+
const brand = (0, brand_store_1.readBrandFromDir)(dir);
|
|
722
|
+
if (brand)
|
|
723
|
+
return brand;
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Directory an org-brand edit should be written to. Mirrors org-context write
|
|
729
|
+
* resolution: the repo-local override dir when the repo carries its own org
|
|
730
|
+
* context, otherwise the portable user-level context dir (where org onboarding
|
|
731
|
+
* writes). Never the synced org cache (managed, overwritten on sync).
|
|
732
|
+
*/
|
|
733
|
+
function resolveOrgBrandWriteDir(workspaceRoot) {
|
|
734
|
+
const loc = resolveTeamContextFile(workspaceRoot, 'org');
|
|
735
|
+
if (loc.writePath)
|
|
736
|
+
return (0, path_1.dirname)(loc.writePath);
|
|
737
|
+
return (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'context');
|
|
738
|
+
}
|
|
698
739
|
/**
|
|
699
740
|
* Count the number of `## [P-...]`-style entries inside a preserved learning
|
|
700
741
|
* file. Returns 0 when the file is absent or unreadable. Counts ALL entries
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.198",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/public/ai-hub/index.html
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
// #700: set the theme pre-paint (no flash). Explicit choice in
|
|
13
13
|
// localStorage wins; otherwise follow the OS preference.
|
|
14
14
|
try{var t=localStorage.getItem('fraim-theme');if(t!=='light'&&t!=='dark'){t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';}h.setAttribute('data-theme',t);}catch(e){h.setAttribute('data-theme','light');}
|
|
15
|
+
// #744: apply the cached org brand pre-paint (no flash of FRAIM identity
|
|
16
|
+
// before company branding). script.js refines the per-theme accent on load.
|
|
17
|
+
try{var b=JSON.parse(localStorage.getItem('fraim-org-brand')||'null');if(b){if(typeof b.color==='string'&&/^#[0-9a-f]{6}$/i.test(b.color)){h.style.setProperty('--accent',b.color);h.style.setProperty('--accent-strong',b.color);}if(typeof b.name==='string'&&b.name){document.title=b.name+' Hub';}}}catch(e){}
|
|
15
18
|
})();</script>
|
|
16
19
|
<link rel="stylesheet" href="./styles.css?v=conv-panels-20260611b">
|
|
17
20
|
<link rel="stylesheet" href="./review.css">
|
|
@@ -20,10 +23,15 @@
|
|
|
20
23
|
|
|
21
24
|
<!-- Issue #512: three-area top nav + account menu (surface=hub only). -->
|
|
22
25
|
<nav class="hub-tabs" id="hub-tabs" hidden>
|
|
26
|
+
<!-- #744: company brand lockup (logo + name), populated from bootstrap.orgBrand. -->
|
|
27
|
+
<span class="hub-brand" id="hub-brand" hidden></span>
|
|
28
|
+
<span class="hub-brand-divider" id="hub-brand-divider" hidden></span>
|
|
23
29
|
<button class="hub-tab" type="button" data-area="company">Company</button>
|
|
24
30
|
<button class="hub-tab" type="button" data-area="manager">Manager</button>
|
|
25
31
|
<button class="hub-tab on" type="button" data-area="projects">Projects</button>
|
|
26
32
|
<div class="nav-right">
|
|
33
|
+
<!-- #744: FRAIM co-mark (co-branding, not white-label) shown when a brand is set. -->
|
|
34
|
+
<span class="hub-cobrand" id="hub-cobrand" hidden></span>
|
|
27
35
|
<button class="avatar-btn" id="avatar-btn" type="button" title="Account & settings">SM</button>
|
|
28
36
|
<div id="account-menu" class="account-menu">
|
|
29
37
|
<div class="am-header">
|
|
@@ -68,6 +76,12 @@
|
|
|
68
76
|
<div class="area-h1">Company</div>
|
|
69
77
|
<p class="area-lede">Your organization's identity — what you do, how you operate, and the guardrails every employee follows on every job.</p>
|
|
70
78
|
<div id="company-push-banner"></div>
|
|
79
|
+
<!-- #744: Brand editor — company identity across the Hub. -->
|
|
80
|
+
<details class="ctx-acc" id="company-brand-acc">
|
|
81
|
+
<summary><span class="ca-chev">▸</span> <span>🎨 Brand</span>
|
|
82
|
+
<span class="ca-note">— your company's identity across the Hub, applied for every teammate</span></summary>
|
|
83
|
+
<div class="ctx-acc-body"><div class="card area-profile" id="company-brand-editor"></div></div>
|
|
84
|
+
</details>
|
|
71
85
|
<details class="ctx-acc" id="company-ctx-acc">
|
|
72
86
|
<summary><span class="ca-chev">▸</span> <span>🏢 Context & rules</span></summary>
|
|
73
87
|
<div class="ctx-acc-body"><div class="card area-profile" id="company-profile"></div></div>
|
|
@@ -199,6 +213,7 @@
|
|
|
199
213
|
<!-- Onboarding state banner lives ABOVE the accordions so it shows even
|
|
200
214
|
when the Brief is collapsed (so the conversation keeps its height). -->
|
|
201
215
|
<div id="proj-onboarding-banner"></div>
|
|
216
|
+
<div id="hub-agent-setup-panel" class="hub-agent-setup-panel" data-testid="hub-cli-setup-panel" hidden></div>
|
|
202
217
|
<details class="ctx-acc" id="proj-brief-acc">
|
|
203
218
|
<summary><span class="ca-chev">▸</span> <span>📋 Brief</span>
|
|
204
219
|
<span class="ca-note">— this project's context and rules, captured by Project Onboarding</span></summary>
|
|
@@ -839,6 +854,7 @@
|
|
|
839
854
|
<span class="cp-agent-label">Run with:</span>
|
|
840
855
|
<span id="cp-agent-picker" class="cp-agent-pills"></span>
|
|
841
856
|
</div>
|
|
857
|
+
<div id="cp-agent-install-panel" class="cp-agent-install-panel" data-testid="cp-cli-setup-panel" hidden></div>
|
|
842
858
|
</div>
|
|
843
859
|
</div>
|
|
844
860
|
|
|
@@ -868,6 +884,6 @@
|
|
|
868
884
|
</div>
|
|
869
885
|
</div>
|
|
870
886
|
|
|
871
|
-
<script src="./script.js?v=
|
|
887
|
+
<script src="./script.js?v=persona-mgr-del-20260707"></script>
|
|
872
888
|
</body>
|
|
873
889
|
</html>
|