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.
@@ -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 = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
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 || 'https://fraim.wellnessatwork.me';
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
- resolve({
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 += d.toString();
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
- stdoutBuffer += d.toString();
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
- resolve({
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
- resolve({
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(0, 500) }
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
- // Parse all JSON-RPC messages received from stdout
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
- resolve({
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(0, 500) }
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
- resolve({
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
- resolve({
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
- if (!entry.isFile() || !entry.name.endsWith('.md'))
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: `${dirName}/${entry.name}`,
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
- SAFE_PACK_RELATIVE_PATH.test(f.relativePath) &&
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 (!SAFE_PACK_RELATIVE_PATH.test(file.relativePath))
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
- fs_1.default.writeFileSync(destination, decorateManagedOrgFile(file.content, metadata.backend));
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 });
@@ -52,7 +52,7 @@ exports.FIRST_RUN_AGENT_OPTIONS = [
52
52
  },
53
53
  {
54
54
  id: 'copilot-cli',
55
- label: 'GitHub Copilot',
55
+ label: 'GitHub Copilot CLI',
56
56
  detectAliases: ['copilot', 'copilot-cli', 'github copilot cli'],
57
57
  loginCommand: 'copilot login',
58
58
  launchCommand: 'copilot',
@@ -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.196",
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": {
@@ -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 &amp; 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 &amp; 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=conv-persist-20260705"></script>
887
+ <script src="./script.js?v=persona-mgr-del-20260707"></script>
872
888
  </body>
873
889
  </html>