fraim-framework 2.0.74 → 2.0.76

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.
@@ -7,10 +7,10 @@
7
7
  * 1. Accepts MCP requests via stdin/stdout
8
8
  * 2. Proxies to remote FRAIM server
9
9
  * 3. Performs template substitution:
10
- * - Config variables: {{config.path.to.value}}
11
- * - Platform-specific actions: {{get_issue}}, {{create_pr}}, etc.
10
+ * - Proxy config variables: {{proxy.config.path.to.value}}
11
+ * - Platform-specific actions: {{proxy.action.get_issue}}, {{proxy.action.create_pr}}, etc.
12
12
  * 4. Automatically detects and injects machine/repo info for fraim_connect
13
- * 5. Substitutes {{delivery.*}} templates based on user's workingStyle
13
+ * 5. Substitutes {{proxy.delivery.*}} templates based on user's workingStyle
14
14
  * (PR or Conversation from ~/.fraim/config.json). Delivery phases live
15
15
  * server-side in the workflow; the proxy just fills in mode-specific content.
16
16
  */
@@ -35,39 +35,59 @@ class FraimTemplateEngine {
35
35
  constructor(opts) {
36
36
  this.deliveryTemplatesCache = null;
37
37
  this.providerTemplatesCache = {};
38
+ this.deliveryTemplatesLoadAttempted = false;
39
+ this.providerTemplatesLoadAttempted = new Set();
38
40
  this.config = opts.config;
39
41
  this.repoInfo = opts.repoInfo;
40
42
  this.workingStyle = opts.workingStyle;
41
43
  this.projectRoot = opts.projectRoot;
42
44
  this.logFn = opts.logFn || (() => { });
43
45
  }
46
+ setDeliveryTemplates(templates) {
47
+ this.deliveryTemplatesCache = templates;
48
+ this.deliveryTemplatesLoadAttempted = true;
49
+ }
50
+ setProviderTemplates(provider, templates) {
51
+ this.providerTemplatesCache[provider] = templates;
52
+ this.providerTemplatesLoadAttempted.add(provider);
53
+ }
54
+ hasDeliveryTemplates() {
55
+ return this.deliveryTemplatesCache !== null;
56
+ }
57
+ hasProviderTemplates(provider) {
58
+ return !!this.providerTemplatesCache[provider];
59
+ }
60
+ setRepoInfo(repoInfo) {
61
+ this.repoInfo = repoInfo;
62
+ }
63
+ setConfig(config) {
64
+ this.config = config;
65
+ }
44
66
  substituteTemplates(content) {
45
67
  let result = content;
46
- // First, substitute config variables with fallback support
47
- if (this.config) {
48
- result = result.replace(/\{\{config\.([^}|]+)(?:\s*\|\s*"([^"]+)")?\}\}/g, (match, path, fallback) => {
49
- try {
68
+ // First, substitute config variables with fallback support.
69
+ // Fallbacks must work even when local config is unavailable.
70
+ result = result.replace(/\{\{proxy\.config\.([^}|]+)(?:\s*\|\s*"([^"]+)")?\}\}/g, (match, path, fallback) => {
71
+ try {
72
+ if (this.config) {
50
73
  const value = (0, object_utils_1.getNestedValue)(this.config, path.trim());
51
74
  if (value !== undefined) {
52
75
  return typeof value === 'object'
53
76
  ? JSON.stringify(value)
54
77
  : String(value);
55
78
  }
56
- if (fallback !== undefined) {
57
- return fallback;
58
- }
59
- return match;
60
79
  }
61
- catch (error) {
62
- return fallback !== undefined ? fallback : match;
63
- }
64
- });
65
- }
66
- // Second, substitute {{delivery.*}} templates
80
+ return fallback !== undefined ? fallback : match;
81
+ }
82
+ catch (error) {
83
+ return fallback !== undefined ? fallback : match;
84
+ }
85
+ });
86
+ // Second, substitute {{proxy.delivery.*}} templates
67
87
  const deliveryValues = this.loadDeliveryTemplates();
68
88
  if (deliveryValues) {
69
- result = result.replace(/\{\{delivery\.([^}]+)\}\}/g, (match, key) => {
70
- const value = deliveryValues[`delivery.${key.trim()}`];
89
+ result = result.replace(/\{\{proxy\.delivery\.([^}]+)\}\}/g, (match, key) => {
90
+ const value = deliveryValues[`proxy.delivery.${key.trim()}`];
71
91
  return value !== undefined ? value : match;
72
92
  });
73
93
  }
@@ -78,44 +98,12 @@ class FraimTemplateEngine {
78
98
  loadDeliveryTemplates() {
79
99
  if (this.deliveryTemplatesCache)
80
100
  return this.deliveryTemplatesCache;
81
- const filename = this.workingStyle === 'Conversation' ? 'delivery-conversation.json' : 'delivery-pr.json';
82
- try {
83
- let content = null;
84
- // Try framework installation directory first (relative to this file)
85
- // This file is in dist/src/local-mcp-server/, so go up to framework root
86
- const frameworkRoot = (0, path_1.join)(__dirname, '..', '..', '..');
87
- const frameworkPath = (0, path_1.join)(frameworkRoot, 'registry', 'providers', filename);
88
- if ((0, fs_1.existsSync)(frameworkPath)) {
89
- content = (0, fs_1.readFileSync)(frameworkPath, 'utf-8');
90
- this.logFn(`✅ Loaded delivery templates from framework: ${frameworkPath}`);
91
- }
92
- // Fallback: try node_modules if not found in framework root
93
- if (!content) {
94
- const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', filename);
95
- if ((0, fs_1.existsSync)(nodeModulesPath)) {
96
- content = (0, fs_1.readFileSync)(nodeModulesPath, 'utf-8');
97
- this.logFn(`✅ Loaded delivery templates from node_modules: ${nodeModulesPath}`);
98
- }
99
- }
100
- // Last resort: try project root (for custom overrides)
101
- if (!content && this.projectRoot) {
102
- const deliveryPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', filename);
103
- if ((0, fs_1.existsSync)(deliveryPath)) {
104
- content = (0, fs_1.readFileSync)(deliveryPath, 'utf-8');
105
- this.logFn(`✅ Loaded delivery templates from project: ${deliveryPath}`);
106
- }
107
- }
108
- if (content) {
109
- this.deliveryTemplatesCache = JSON.parse(content);
110
- return this.deliveryTemplatesCache;
111
- }
112
- this.logFn(`⚠️ Could not find delivery templates: ${filename}`);
101
+ if (this.deliveryTemplatesLoadAttempted)
113
102
  return null;
114
- }
115
- catch (error) {
116
- this.logFn(`⚠️ Failed to load delivery templates: ${error.message}`);
117
- return null;
118
- }
103
+ this.deliveryTemplatesLoadAttempted = true;
104
+ // Server-authoritative mode: runtime templates are hydrated via cache + remote fetch
105
+ // in FraimLocalMCPServer. Do not read framework/project registry files directly.
106
+ return null;
119
107
  }
120
108
  substitutePlatformActions(content) {
121
109
  const provider = (0, provider_utils_1.detectProvider)(this.repoInfo?.url);
@@ -124,7 +112,8 @@ class FraimTemplateEngine {
124
112
  return content;
125
113
  let result = content;
126
114
  for (const [action, template] of Object.entries(templates)) {
127
- const regex = new RegExp(`\\{\\{${action}\\}\\}`, 'g');
115
+ const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
+ const regex = new RegExp(`\\{\\{${FraimTemplateEngine.PROXY_ACTION_PREFIX}${escapedAction}\\}\\}`, 'g');
128
117
  const renderedTemplate = this.renderActionTemplate(template);
129
118
  result = result.replace(regex, renderedTemplate);
130
119
  }
@@ -133,39 +122,12 @@ class FraimTemplateEngine {
133
122
  loadProviderTemplates(provider) {
134
123
  if (this.providerTemplatesCache[provider])
135
124
  return this.providerTemplatesCache[provider];
136
- try {
137
- let content = null;
138
- // Try framework installation directory first (relative to this file)
139
- const frameworkRoot = (0, path_1.join)(__dirname, '..', '..', '..');
140
- const frameworkPath = (0, path_1.join)(frameworkRoot, 'registry', 'providers', `${provider}.json`);
141
- if ((0, fs_1.existsSync)(frameworkPath)) {
142
- content = (0, fs_1.readFileSync)(frameworkPath, 'utf-8');
143
- }
144
- // Fallback: try node_modules
145
- if (!content) {
146
- const nodeModulesPath = (0, path_1.join)(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry', 'providers', `${provider}.json`);
147
- if ((0, fs_1.existsSync)(nodeModulesPath)) {
148
- content = (0, fs_1.readFileSync)(nodeModulesPath, 'utf-8');
149
- }
150
- }
151
- // Last resort: try project root (for custom overrides)
152
- if (!content && this.projectRoot) {
153
- const providerPath = (0, path_1.join)(this.projectRoot, 'registry', 'providers', `${provider}.json`);
154
- if ((0, fs_1.existsSync)(providerPath)) {
155
- content = (0, fs_1.readFileSync)(providerPath, 'utf-8');
156
- }
157
- }
158
- if (content) {
159
- const templates = JSON.parse(content);
160
- this.providerTemplatesCache[provider] = templates;
161
- return templates;
162
- }
163
- return null;
164
- }
165
- catch (error) {
166
- this.logFn(`⚠️ Failed to load provider templates: ${error.message}`);
125
+ if (this.providerTemplatesLoadAttempted.has(provider))
167
126
  return null;
168
- }
127
+ this.providerTemplatesLoadAttempted.add(provider);
128
+ // Server-authoritative mode: runtime templates are hydrated via cache + remote fetch
129
+ // in FraimLocalMCPServer. Do not read framework/project registry files directly.
130
+ return null;
169
131
  }
170
132
  renderActionTemplate(template) {
171
133
  if (!this.repoInfo && !this.config?.repository) {
@@ -173,8 +135,8 @@ class FraimTemplateEngine {
173
135
  }
174
136
  return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
175
137
  const trimmedPath = path.trim();
176
- if (trimmedPath.startsWith('repository.')) {
177
- const repoPath = trimmedPath.substring('repository.'.length);
138
+ if (trimmedPath.startsWith('proxy.repository.')) {
139
+ const repoPath = trimmedPath.substring('proxy.repository.'.length);
178
140
  if (this.repoInfo) {
179
141
  const value = (0, object_utils_1.getNestedValue)(this.repoInfo, repoPath);
180
142
  if (value !== undefined)
@@ -191,6 +153,7 @@ class FraimTemplateEngine {
191
153
  }
192
154
  }
193
155
  exports.FraimTemplateEngine = FraimTemplateEngine;
156
+ FraimTemplateEngine.PROXY_ACTION_PREFIX = 'proxy.action.';
194
157
  class FraimLocalMCPServer {
195
158
  constructor() {
196
159
  this.config = null;
@@ -200,7 +163,14 @@ class FraimLocalMCPServer {
200
163
  this.machineInfo = null;
201
164
  this.repoInfo = null;
202
165
  this.engine = null;
203
- this.registryResolver = null;
166
+ this.fallbackRewriteCount = 0;
167
+ this.fallbackRewriteRequestCount = 0;
168
+ this.fallbackRewriteTokens = new Map();
169
+ this.pendingFallbackRequestCount = 0;
170
+ this.pendingFallbackTokenRewriteCount = 0;
171
+ this.pendingFallbackTokenCounts = new Map();
172
+ this.pendingFallbackEvents = [];
173
+ this.fallbackSummaryEmitted = false;
204
174
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
205
175
  this.apiKey = process.env.FRAIM_API_KEY || '';
206
176
  this.localVersion = this.detectLocalVersion();
@@ -331,12 +301,19 @@ class FraimLocalMCPServer {
331
301
  this.log('✅ Loaded local .fraim/config.json');
332
302
  }
333
303
  else {
304
+ this.config = null;
334
305
  this.log('⚠️ No .fraim/config.json found - template substitution disabled');
335
306
  }
307
+ if (this.engine) {
308
+ this.engine.setConfig(this.config);
309
+ }
336
310
  }
337
311
  catch (error) {
338
312
  this.logError(`Failed to load config: ${error}`);
339
313
  this.config = null;
314
+ if (this.engine) {
315
+ this.engine.setConfig(this.config);
316
+ }
340
317
  }
341
318
  }
342
319
  /**
@@ -479,91 +456,340 @@ class FraimLocalMCPServer {
479
456
  }
480
457
  return 'PR';
481
458
  }
482
- substituteTemplates(content) {
459
+ getHomeDir() {
460
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
461
+ return homeDir || null;
462
+ }
463
+ getProviderCachePath(filename) {
464
+ const homeDir = this.getHomeDir();
465
+ if (!homeDir)
466
+ return null;
467
+ return (0, path_1.join)(homeDir, '.fraim', 'cache', 'registry', 'providers', filename);
468
+ }
469
+ readCachedTemplateFile(filename) {
470
+ try {
471
+ const cachePath = this.getProviderCachePath(filename);
472
+ if (!cachePath || !(0, fs_1.existsSync)(cachePath))
473
+ return null;
474
+ const content = (0, fs_1.readFileSync)(cachePath, 'utf8');
475
+ const parsed = JSON.parse(content);
476
+ if (parsed && typeof parsed === 'object') {
477
+ this.log(`✅ Loaded template cache: ${cachePath}`);
478
+ return parsed;
479
+ }
480
+ }
481
+ catch (error) {
482
+ this.log(`⚠️ Failed to read template cache ${filename}: ${error.message}`);
483
+ }
484
+ return null;
485
+ }
486
+ writeCachedTemplateFile(filename, templates) {
487
+ try {
488
+ const cachePath = this.getProviderCachePath(filename);
489
+ if (!cachePath)
490
+ return;
491
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(cachePath), { recursive: true });
492
+ (0, fs_1.writeFileSync)(cachePath, JSON.stringify(templates), 'utf8');
493
+ this.log(`✅ Cached template file: ${cachePath}`);
494
+ }
495
+ catch (error) {
496
+ this.log(`⚠️ Failed to cache template ${filename}: ${error.message}`);
497
+ }
498
+ }
499
+ ensureEngine() {
483
500
  if (!this.engine) {
501
+ const repoInfo = this.detectRepoInfo();
484
502
  this.engine = new FraimTemplateEngine({
485
503
  config: this.config,
486
- repoInfo: this.detectRepoInfo(),
504
+ repoInfo,
487
505
  workingStyle: this.getWorkingStyle(),
488
506
  projectRoot: this.findProjectRoot(),
489
507
  logFn: (msg) => this.log(msg)
490
508
  });
491
509
  }
492
- return this.engine.substituteTemplates(content);
510
+ else {
511
+ this.engine.setConfig(this.config);
512
+ if (this.repoInfo) {
513
+ this.engine.setRepoInfo(this.repoInfo);
514
+ }
515
+ }
516
+ return this.engine;
517
+ }
518
+ collectStrings(value, output) {
519
+ if (typeof value === 'string') {
520
+ output.push(value);
521
+ return;
522
+ }
523
+ if (Array.isArray(value)) {
524
+ value.forEach(item => this.collectStrings(item, output));
525
+ return;
526
+ }
527
+ if (value && typeof value === 'object') {
528
+ Object.values(value).forEach(item => this.collectStrings(item, output));
529
+ }
530
+ }
531
+ responseContainsDeliveryPlaceholders(response) {
532
+ if (!response.result)
533
+ return false;
534
+ const strings = [];
535
+ this.collectStrings(response.result, strings);
536
+ return strings.some(s => /\{\{proxy\.delivery\.[^}]+\}\}/.test(s));
537
+ }
538
+ responseHasPotentialProviderPlaceholders(response) {
539
+ if (!response.result)
540
+ return false;
541
+ const strings = [];
542
+ this.collectStrings(response.result, strings);
543
+ return strings.some(s => /\{\{proxy\.action\.[^}]+\}\}/.test(s));
544
+ }
545
+ async fetchRegistryFileFromServer(path, requestSessionId) {
546
+ const request = {
547
+ jsonrpc: '2.0',
548
+ id: (0, crypto_1.randomUUID)(),
549
+ method: 'tools/call',
550
+ params: {
551
+ name: 'get_fraim_file',
552
+ arguments: { path }
553
+ }
554
+ };
555
+ this.applyRequestSessionId(request, requestSessionId);
556
+ const response = await this.proxyToRemote(request);
557
+ if (response.error) {
558
+ this.log(`⚠️ Failed to fetch registry file ${path}: ${response.error.message}`);
559
+ return null;
560
+ }
561
+ const textBlock = response.result?.content?.find((c) => c.type === 'text');
562
+ if (!textBlock?.text) {
563
+ this.log(`⚠️ No text content in registry response for ${path}`);
564
+ return null;
565
+ }
566
+ return textBlock.text;
567
+ }
568
+ parseTemplateJson(content, context) {
569
+ const candidates = [];
570
+ const trimmed = content.trim();
571
+ candidates.push(trimmed);
572
+ // get_fraim_file responses include a markdown header, then a separator line, then file content.
573
+ const separatorIndex = trimmed.indexOf('\n---\n');
574
+ if (separatorIndex >= 0) {
575
+ candidates.push(trimmed.slice(separatorIndex + '\n---\n'.length).trim());
576
+ }
577
+ const codeFenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
578
+ if (codeFenceMatch?.[1]) {
579
+ candidates.push(codeFenceMatch[1].trim());
580
+ }
581
+ const firstBrace = trimmed.indexOf('{');
582
+ const lastBrace = trimmed.lastIndexOf('}');
583
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
584
+ candidates.push(trimmed.slice(firstBrace, lastBrace + 1));
585
+ }
586
+ for (const candidate of candidates) {
587
+ if (!candidate)
588
+ continue;
589
+ try {
590
+ const parsed = JSON.parse(candidate);
591
+ if (parsed && typeof parsed === 'object') {
592
+ return parsed;
593
+ }
594
+ }
595
+ catch {
596
+ // Try the next extraction candidate.
597
+ }
598
+ }
599
+ this.log(`⚠️ Failed to parse ${context}: no valid JSON object found in response`);
600
+ return null;
601
+ }
602
+ async ensureDeliveryTemplatesAvailable(response, requestSessionId) {
603
+ if (!this.responseContainsDeliveryPlaceholders(response))
604
+ return;
605
+ const engine = this.ensureEngine();
606
+ if (engine.hasDeliveryTemplates())
607
+ return;
608
+ if (engine.loadDeliveryTemplates())
609
+ return;
610
+ const filename = this.getWorkingStyle() === 'Conversation'
611
+ ? 'delivery-conversation.json'
612
+ : 'delivery-pr.json';
613
+ const cached = this.readCachedTemplateFile(filename);
614
+ if (cached) {
615
+ engine.setDeliveryTemplates(cached);
616
+ return;
617
+ }
618
+ const remoteContent = await this.fetchRegistryFileFromServer(`providers/${filename}`, requestSessionId);
619
+ if (!remoteContent)
620
+ return;
621
+ const parsed = this.parseTemplateJson(remoteContent, `providers/${filename}`);
622
+ if (!parsed)
623
+ return;
624
+ engine.setDeliveryTemplates(parsed);
625
+ this.writeCachedTemplateFile(filename, parsed);
626
+ }
627
+ async ensureProviderTemplatesAvailable(response, requestSessionId) {
628
+ const hasDirectProviderPlaceholders = this.responseHasPotentialProviderPlaceholders(response);
629
+ const hasDeliveryPlaceholders = this.responseContainsDeliveryPlaceholders(response);
630
+ if (!hasDirectProviderPlaceholders && !hasDeliveryPlaceholders)
631
+ return;
632
+ const engine = this.ensureEngine();
633
+ const provider = (0, provider_utils_1.detectProvider)(this.detectRepoInfo()?.url);
634
+ if (engine.hasProviderTemplates(provider))
635
+ return;
636
+ if (engine.loadProviderTemplates(provider))
637
+ return;
638
+ const filename = `${provider}.json`;
639
+ const cached = this.readCachedTemplateFile(filename);
640
+ if (cached) {
641
+ engine.setProviderTemplates(provider, cached);
642
+ return;
643
+ }
644
+ const remoteContent = await this.fetchRegistryFileFromServer(`providers/${filename}`, requestSessionId);
645
+ if (!remoteContent)
646
+ return;
647
+ const parsed = this.parseTemplateJson(remoteContent, `providers/${filename}`);
648
+ if (!parsed)
649
+ return;
650
+ engine.setProviderTemplates(provider, parsed);
651
+ this.writeCachedTemplateFile(filename, parsed);
652
+ }
653
+ async hydrateTemplateCachesForResponse(response, requestSessionId) {
654
+ if (!response.result)
655
+ return;
656
+ await this.ensureDeliveryTemplatesAvailable(response, requestSessionId);
657
+ await this.ensureProviderTemplatesAvailable(response, requestSessionId);
658
+ }
659
+ async processResponseWithHydration(response, requestSessionId) {
660
+ await this.hydrateTemplateCachesForResponse(response, requestSessionId);
661
+ let processedResponse = this.processResponse(response);
662
+ // Delivery substitution can introduce provider action placeholders (e.g. {{proxy.action.update_issue_status}})
663
+ // that were not visible pre-substitution. Run one targeted re-hydration pass if needed.
664
+ const provider = (0, provider_utils_1.detectProvider)(this.detectRepoInfo()?.url);
665
+ const engine = this.ensureEngine();
666
+ if (this.responseHasPotentialProviderPlaceholders(processedResponse) &&
667
+ !engine.hasProviderTemplates(provider)) {
668
+ await this.ensureProviderTemplatesAvailable(processedResponse, requestSessionId);
669
+ processedResponse = this.processResponse(processedResponse);
670
+ }
671
+ return this.applyAgentFallbackForUnresolvedProxy(processedResponse);
672
+ }
673
+ rewriteProxyTokensInText(text) {
674
+ const tokens = new Set();
675
+ const rewritten = text.replace(/\{\{\s*proxy\.([^}]+?)\s*\}\}/g, (_match, proxyPath) => {
676
+ const normalized = proxyPath.trim();
677
+ tokens.add(`proxy.${normalized}`);
678
+ return `{{agent.${normalized}}}`;
679
+ });
680
+ if (tokens.size === 0) {
681
+ return { text, tokens: [] };
682
+ }
683
+ const hasResolutionNotice = rewritten.includes('## Agent Resolution Needed');
684
+ const finalText = hasResolutionNotice
685
+ ? rewritten
686
+ : `${FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE}\n\n${rewritten}`;
687
+ return { text: finalText, tokens: Array.from(tokens).sort() };
688
+ }
689
+ rewriteUnresolvedProxyPlaceholders(value) {
690
+ const rewrittenTokens = new Set();
691
+ const rewrite = (input) => {
692
+ if (typeof input === 'string') {
693
+ const rewritten = this.rewriteProxyTokensInText(input);
694
+ rewritten.tokens.forEach((token) => rewrittenTokens.add(token));
695
+ return rewritten.text;
696
+ }
697
+ if (Array.isArray(input)) {
698
+ return input.map(rewrite);
699
+ }
700
+ if (input && typeof input === 'object') {
701
+ const output = {};
702
+ for (const [key, nestedValue] of Object.entries(input)) {
703
+ if (key === 'text' && typeof nestedValue === 'string') {
704
+ const rewritten = this.rewriteProxyTokensInText(nestedValue);
705
+ rewritten.tokens.forEach((token) => rewrittenTokens.add(token));
706
+ output[key] = rewritten.text;
707
+ }
708
+ else {
709
+ output[key] = rewrite(nestedValue);
710
+ }
711
+ }
712
+ return output;
713
+ }
714
+ return input;
715
+ };
716
+ const rewrittenValue = rewrite(value);
717
+ return { value: rewrittenValue, tokens: Array.from(rewrittenTokens).sort() };
718
+ }
719
+ substituteTemplates(content) {
720
+ return this.ensureEngine().substituteTemplates(content);
493
721
  }
494
722
  /**
495
723
  * Initialize the LocalRegistryResolver for override resolution
496
724
  */
497
- getRegistryResolver() {
498
- if (!this.registryResolver) {
499
- const projectRoot = this.findProjectRoot();
500
- this.log(`🔍 getRegistryResolver: projectRoot = ${projectRoot}`);
501
- if (!projectRoot) {
502
- this.log('⚠️ No project root found, override resolution disabled');
503
- // Return a resolver that always falls back to remote
504
- this.registryResolver = new local_registry_resolver_1.LocalRegistryResolver({
505
- workspaceRoot: process.cwd(),
506
- remoteContentResolver: async (path) => {
507
- throw new Error('No project root available');
725
+ getRegistryResolver(requestSessionId) {
726
+ const projectRoot = this.findProjectRoot();
727
+ this.log(`🔍 getRegistryResolver: projectRoot = ${projectRoot}`);
728
+ if (!projectRoot) {
729
+ this.log('⚠️ No project root found, override resolution disabled');
730
+ // Return a resolver that always falls back to remote
731
+ return new local_registry_resolver_1.LocalRegistryResolver({
732
+ workspaceRoot: process.cwd(),
733
+ remoteContentResolver: async (_path) => {
734
+ throw new Error('No project root available');
735
+ }
736
+ });
737
+ }
738
+ else {
739
+ return new local_registry_resolver_1.LocalRegistryResolver({
740
+ workspaceRoot: projectRoot,
741
+ remoteContentResolver: async (path) => {
742
+ // Fetch parent content from remote for inheritance
743
+ this.log(`🔄 Remote content resolver: fetching ${path}`);
744
+ let request;
745
+ if (path.startsWith('workflows/')) {
746
+ // Extract workflow name from path: workflows/category/name.md -> name
747
+ const pathParts = path.replace('workflows/', '').replace('.md', '').split('/');
748
+ const workflowName = pathParts[pathParts.length - 1]; // Get last part (name)
749
+ this.log(`🔄 Fetching workflow: ${workflowName}`);
750
+ request = {
751
+ jsonrpc: '2.0',
752
+ id: (0, crypto_1.randomUUID)(),
753
+ method: 'tools/call',
754
+ params: {
755
+ name: 'get_fraim_workflow',
756
+ arguments: { workflow: workflowName }
757
+ }
758
+ };
508
759
  }
509
- });
510
- }
511
- else {
512
- this.registryResolver = new local_registry_resolver_1.LocalRegistryResolver({
513
- workspaceRoot: projectRoot,
514
- remoteContentResolver: async (path) => {
515
- // Fetch parent content from remote for inheritance
516
- this.log(`🔄 Remote content resolver: fetching ${path}`);
517
- let request;
518
- if (path.startsWith('workflows/')) {
519
- // Extract workflow name from path: workflows/category/name.md -> name
520
- const pathParts = path.replace('workflows/', '').replace('.md', '').split('/');
521
- const workflowName = pathParts[pathParts.length - 1]; // Get last part (name)
522
- this.log(`🔄 Fetching workflow: ${workflowName}`);
523
- request = {
524
- jsonrpc: '2.0',
525
- id: (0, crypto_1.randomUUID)(),
526
- method: 'tools/call',
527
- params: {
528
- name: 'get_fraim_workflow',
529
- arguments: { workflow: workflowName }
530
- }
531
- };
532
- }
533
- else {
534
- // For non-workflow files (templates, rules, etc.), use get_fraim_file
535
- this.log(`🔄 Fetching file: ${path}`);
536
- request = {
537
- jsonrpc: '2.0',
538
- id: (0, crypto_1.randomUUID)(),
539
- method: 'tools/call',
540
- params: {
541
- name: 'get_fraim_file',
542
- arguments: { path }
543
- }
544
- };
545
- }
546
- const response = await this.proxyToRemote(request);
547
- if (response.error) {
548
- this.logError(`❌ Remote content resolver failed: ${response.error.message}`);
549
- throw new Error(`Failed to fetch parent: ${response.error.message}`);
550
- }
551
- // Extract content from MCP response format
552
- if (response.result?.content && Array.isArray(response.result.content)) {
553
- const textContent = response.result.content.find((c) => c.type === 'text');
554
- if (textContent?.text) {
555
- this.log(`✅ Remote content resolver: fetched ${textContent.text.length} chars`);
556
- return textContent.text;
760
+ else {
761
+ // For non-workflow files (templates, rules, etc.), use get_fraim_file
762
+ this.log(`🔄 Fetching file: ${path}`);
763
+ request = {
764
+ jsonrpc: '2.0',
765
+ id: (0, crypto_1.randomUUID)(),
766
+ method: 'tools/call',
767
+ params: {
768
+ name: 'get_fraim_file',
769
+ arguments: { path }
557
770
  }
771
+ };
772
+ }
773
+ this.applyRequestSessionId(request, requestSessionId);
774
+ const response = await this.proxyToRemote(request);
775
+ if (response.error) {
776
+ this.logError(`❌ Remote content resolver failed: ${response.error.message}`);
777
+ throw new Error(`Failed to fetch parent: ${response.error.message}`);
778
+ }
779
+ // Extract content from MCP response format
780
+ if (response.result?.content && Array.isArray(response.result.content)) {
781
+ const textContent = response.result.content.find((c) => c.type === 'text');
782
+ if (textContent?.text) {
783
+ this.log(`✅ Remote content resolver: fetched ${textContent.text.length} chars`);
784
+ return textContent.text;
558
785
  }
559
- this.logError(`❌ Remote content resolver: no content in response for ${path}`);
560
- this.logError(`Response: ${JSON.stringify(response, null, 2)}`);
561
- throw new Error(`No content in remote response for ${path}`);
562
786
  }
563
- });
564
- }
787
+ this.logError(`❌ Remote content resolver: no content in response for ${path}`);
788
+ this.logError(`Response: ${JSON.stringify(response, null, 2)}`);
789
+ throw new Error(`No content in remote response for ${path}`);
790
+ }
791
+ });
565
792
  }
566
- return this.registryResolver;
567
793
  }
568
794
  /**
569
795
  * Determine workflow category from workflow name
@@ -625,11 +851,121 @@ class FraimLocalMCPServer {
625
851
  result: processValue(response.result)
626
852
  };
627
853
  }
854
+ applyAgentFallbackForUnresolvedProxy(response) {
855
+ if (!response.result)
856
+ return response;
857
+ const rewritten = this.rewriteUnresolvedProxyPlaceholders(response.result);
858
+ if (rewritten.tokens.length > 0) {
859
+ this.recordFallbackRewrite(rewritten.tokens);
860
+ this.logError(`[${FraimLocalMCPServer.FALLBACK_ALERT_MARKER}] Rewrote unresolved proxy placeholders to agent placeholders: ${rewritten.tokens.join(', ')}`);
861
+ }
862
+ return {
863
+ ...response,
864
+ result: rewritten.value
865
+ };
866
+ }
867
+ recordFallbackRewrite(tokens) {
868
+ this.fallbackRewriteRequestCount += 1;
869
+ this.fallbackRewriteCount += tokens.length;
870
+ for (const token of tokens) {
871
+ const existing = this.fallbackRewriteTokens.get(token) || 0;
872
+ this.fallbackRewriteTokens.set(token, existing + 1);
873
+ }
874
+ this.pendingFallbackRequestCount += 1;
875
+ this.pendingFallbackTokenRewriteCount += tokens.length;
876
+ for (const token of tokens) {
877
+ const existing = this.pendingFallbackTokenCounts.get(token) || 0;
878
+ this.pendingFallbackTokenCounts.set(token, existing + 1);
879
+ }
880
+ this.pendingFallbackEvents.push({
881
+ at: new Date().toISOString(),
882
+ tokenCount: tokens.length,
883
+ tokens: [...tokens]
884
+ });
885
+ if (this.pendingFallbackEvents.length > 250) {
886
+ this.pendingFallbackEvents = this.pendingFallbackEvents.slice(-250);
887
+ }
888
+ }
889
+ buildPendingFallbackTelemetryPayload() {
890
+ if (this.pendingFallbackRequestCount === 0 &&
891
+ this.pendingFallbackTokenRewriteCount === 0 &&
892
+ this.pendingFallbackEvents.length === 0) {
893
+ return null;
894
+ }
895
+ const tokenCounts = {};
896
+ for (const [token, count] of this.pendingFallbackTokenCounts.entries()) {
897
+ tokenCounts[token] = count;
898
+ }
899
+ return {
900
+ requestFallbacks: this.pendingFallbackRequestCount,
901
+ tokenRewrites: this.pendingFallbackTokenRewriteCount,
902
+ tokenCounts,
903
+ events: this.pendingFallbackEvents.map((event) => ({
904
+ at: event.at,
905
+ tokenCount: event.tokenCount,
906
+ tokens: [...event.tokens]
907
+ }))
908
+ };
909
+ }
910
+ acknowledgePendingFallbackTelemetry(payload) {
911
+ this.pendingFallbackRequestCount = Math.max(0, this.pendingFallbackRequestCount - payload.requestFallbacks);
912
+ this.pendingFallbackTokenRewriteCount = Math.max(0, this.pendingFallbackTokenRewriteCount - payload.tokenRewrites);
913
+ for (const [token, sentCount] of Object.entries(payload.tokenCounts)) {
914
+ const current = this.pendingFallbackTokenCounts.get(token) || 0;
915
+ const next = Math.max(0, current - Math.max(0, sentCount));
916
+ if (next === 0) {
917
+ this.pendingFallbackTokenCounts.delete(token);
918
+ }
919
+ else {
920
+ this.pendingFallbackTokenCounts.set(token, next);
921
+ }
922
+ }
923
+ if (payload.events.length > 0) {
924
+ this.pendingFallbackEvents.splice(0, Math.min(payload.events.length, this.pendingFallbackEvents.length));
925
+ }
926
+ }
927
+ emitFallbackSummary(reason) {
928
+ if (this.fallbackSummaryEmitted)
929
+ return;
930
+ this.fallbackSummaryEmitted = true;
931
+ if (this.fallbackRewriteCount === 0)
932
+ return;
933
+ const topTokens = Array.from(this.fallbackRewriteTokens.entries())
934
+ .sort((a, b) => b[1] - a[1])
935
+ .slice(0, 10)
936
+ .map(([token, count]) => `${token}:${count}`)
937
+ .join(', ');
938
+ this.logError(`[${FraimLocalMCPServer.FALLBACK_SUMMARY_MARKER}] reason=${reason} requestFallbacks=${this.fallbackRewriteRequestCount} tokenRewrites=${this.fallbackRewriteCount} uniqueTokens=${this.fallbackRewriteTokens.size}${topTokens ? ` topTokens=${topTokens}` : ''}`);
939
+ }
940
+ extractSessionIdFromRequest(request) {
941
+ const sessionIdFromParams = request.params?.sessionId;
942
+ if (typeof sessionIdFromParams === 'string' && sessionIdFromParams.trim().length > 0) {
943
+ return sessionIdFromParams.trim();
944
+ }
945
+ const sessionIdFromArgs = request.params?.arguments?.sessionId;
946
+ if (typeof sessionIdFromArgs === 'string' && sessionIdFromArgs.trim().length > 0) {
947
+ return sessionIdFromArgs.trim();
948
+ }
949
+ return null;
950
+ }
951
+ applyRequestSessionId(request, requestSessionId) {
952
+ if (!requestSessionId || requestSessionId.trim().length === 0)
953
+ return;
954
+ request.params = request.params || {};
955
+ if (typeof request.params.sessionId !== 'string' || request.params.sessionId.trim().length === 0) {
956
+ request.params.sessionId = requestSessionId;
957
+ }
958
+ request.params.arguments = request.params.arguments || {};
959
+ if (typeof request.params.arguments.sessionId !== 'string' || request.params.arguments.sessionId.trim().length === 0) {
960
+ request.params.arguments.sessionId = requestSessionId;
961
+ }
962
+ }
628
963
  /**
629
964
  * Proxy request to remote FRAIM server
630
965
  */
631
966
  async proxyToRemote(request) {
632
967
  const requestId = (0, crypto_1.randomUUID)();
968
+ let sentFallbackTelemetry = null;
633
969
  try {
634
970
  // Special handling for fraim_connect - automatically inject machine and repo info
635
971
  if (request.method === 'tools/call' && request.params?.name === 'fraim_connect') {
@@ -670,19 +1006,38 @@ class FraimLocalMCPServer {
670
1006
  }
671
1007
  this.log(`[req:${requestId}] Using agent-provided repo info: ${args.repo.owner}/${args.repo.name}`);
672
1008
  }
1009
+ // Keep proxy runtime repository context in sync with connect payload.
1010
+ this.repoInfo = args.repo;
1011
+ if (this.engine) {
1012
+ this.engine.setRepoInfo(args.repo);
1013
+ }
673
1014
  // Update the request with injected info
674
1015
  request.params.arguments = args;
675
1016
  }
1017
+ sentFallbackTelemetry = this.buildPendingFallbackTelemetryPayload();
1018
+ const headers = {
1019
+ 'Content-Type': 'application/json',
1020
+ 'x-api-key': this.apiKey,
1021
+ 'x-fraim-request-id': requestId,
1022
+ 'x-fraim-local-version': this.localVersion
1023
+ };
1024
+ const sessionId = this.extractSessionIdFromRequest(request);
1025
+ if (sessionId) {
1026
+ headers[FraimLocalMCPServer.SESSION_HEADER] = sessionId;
1027
+ }
1028
+ if (sentFallbackTelemetry) {
1029
+ headers[FraimLocalMCPServer.FALLBACK_HEADER] = Buffer
1030
+ .from(JSON.stringify(sentFallbackTelemetry), 'utf8')
1031
+ .toString('base64');
1032
+ }
676
1033
  this.log(`[req:${requestId}] Proxying ${request.method} to ${this.remoteUrl}/mcp`);
677
1034
  const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, request, {
678
- headers: {
679
- 'Content-Type': 'application/json',
680
- 'x-api-key': this.apiKey,
681
- 'x-fraim-request-id': requestId,
682
- 'x-fraim-local-version': this.localVersion
683
- },
1035
+ headers,
684
1036
  timeout: 30000
685
1037
  });
1038
+ if (sentFallbackTelemetry) {
1039
+ this.acknowledgePendingFallbackTelemetry(sentFallbackTelemetry);
1040
+ }
686
1041
  return response.data;
687
1042
  }
688
1043
  catch (error) {
@@ -707,6 +1062,10 @@ class FraimLocalMCPServer {
707
1062
  localMcpVersion: this.localVersion
708
1063
  };
709
1064
  }
1065
+ if (sentFallbackTelemetry) {
1066
+ // Request reached the server and produced a response; clear sent fallback delta.
1067
+ this.acknowledgePendingFallbackTelemetry(sentFallbackTelemetry);
1068
+ }
710
1069
  return forwarded;
711
1070
  }
712
1071
  return {
@@ -757,6 +1116,7 @@ class FraimLocalMCPServer {
757
1116
  */
758
1117
  async handleRequest(request) {
759
1118
  this.log(`📥 ${request.method}`);
1119
+ const requestSessionId = this.extractSessionIdFromRequest(request);
760
1120
  // Special handling for initialize request
761
1121
  if (request.method === 'initialize') {
762
1122
  // Check if client supports roots
@@ -767,12 +1127,15 @@ class FraimLocalMCPServer {
767
1127
  }
768
1128
  // Proxy initialize to remote server first
769
1129
  const response = await this.proxyToRemote(request);
770
- const processedResponse = this.processResponse(response);
1130
+ const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
771
1131
  // After successful initialization, load config
772
1132
  if (!processedResponse.error) {
773
- // For now, don't request roots - just use env var + upward search
774
- // TODO: Implement roots/list properly after initialization is complete
1133
+ // Load config immediately for compatibility, then request roots so
1134
+ // workspace-aware clients (Codex, Cursor, etc.) can provide exact roots.
775
1135
  this.loadConfig();
1136
+ if (this.clientSupportsRoots) {
1137
+ setImmediate(() => this.requestRootsFromClient());
1138
+ }
776
1139
  }
777
1140
  this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
778
1141
  return processedResponse;
@@ -797,7 +1160,7 @@ class FraimLocalMCPServer {
797
1160
  const category = this.getWorkflowCategory(workflowName);
798
1161
  requestedPath = `workflows/${category}/${workflowName}.md`;
799
1162
  this.log(`🔍 Checking for override: ${requestedPath}`);
800
- const resolver = this.getRegistryResolver();
1163
+ const resolver = this.getRegistryResolver(requestSessionId);
801
1164
  const hasOverride = resolver.hasLocalOverride(requestedPath);
802
1165
  this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
803
1166
  if (hasOverride) {
@@ -818,7 +1181,7 @@ class FraimLocalMCPServer {
818
1181
  }
819
1182
  };
820
1183
  // Apply template substitution
821
- const processedResponse = this.processResponse(response);
1184
+ const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
822
1185
  this.log(`📤 ${request.method} → OK`);
823
1186
  return processedResponse;
824
1187
  }
@@ -830,31 +1193,37 @@ class FraimLocalMCPServer {
830
1193
  this.log('⚠️ No path provided in get_fraim_file');
831
1194
  }
832
1195
  else {
833
- this.log(`🔍 Checking for override: ${requestedPath}`);
834
- const resolver = this.getRegistryResolver();
835
- const hasOverride = resolver.hasLocalOverride(requestedPath);
836
- this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
837
- if (hasOverride) {
838
- this.log(`✅ Local override found: ${requestedPath}`);
839
- const resolved = await resolver.resolveFile(requestedPath);
840
- this.log(`📝 Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
841
- // Build MCP response with resolved content
842
- const response = {
843
- jsonrpc: '2.0',
844
- id: request.id,
845
- result: {
846
- content: [
847
- {
848
- type: 'text',
849
- text: resolved.content
850
- }
851
- ]
852
- }
853
- };
854
- // Apply template substitution
855
- const processedResponse = this.processResponse(response);
856
- this.log(`📤 ${request.method} → OK`);
857
- return processedResponse;
1196
+ if (requestedPath.startsWith('providers/')) {
1197
+ // Server-authoritative templates: never resolve provider files via local overrides.
1198
+ this.log(`🔒 Skipping local override for provider template file: ${requestedPath}`);
1199
+ }
1200
+ else {
1201
+ this.log(`🔍 Checking for override: ${requestedPath}`);
1202
+ const resolver = this.getRegistryResolver(requestSessionId);
1203
+ const hasOverride = resolver.hasLocalOverride(requestedPath);
1204
+ this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1205
+ if (hasOverride) {
1206
+ this.log(`✅ Local override found: ${requestedPath}`);
1207
+ const resolved = await resolver.resolveFile(requestedPath);
1208
+ this.log(`📝 Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1209
+ // Build MCP response with resolved content
1210
+ const response = {
1211
+ jsonrpc: '2.0',
1212
+ id: request.id,
1213
+ result: {
1214
+ content: [
1215
+ {
1216
+ type: 'text',
1217
+ text: resolved.content
1218
+ }
1219
+ ]
1220
+ }
1221
+ };
1222
+ // Apply template substitution
1223
+ const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
1224
+ this.log(`📤 ${request.method} → OK`);
1225
+ return processedResponse;
1226
+ }
858
1227
  }
859
1228
  }
860
1229
  }
@@ -867,8 +1236,7 @@ class FraimLocalMCPServer {
867
1236
  }
868
1237
  // Proxy to remote server
869
1238
  const response = await this.proxyToRemote(request);
870
- // Process template substitution (config vars, platform actions, delivery templates)
871
- const processedResponse = this.processResponse(response);
1239
+ const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
872
1240
  this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
873
1241
  return processedResponse;
874
1242
  }
@@ -891,11 +1259,12 @@ class FraimLocalMCPServer {
891
1259
  // Use the first root (typically the primary workspace folder)
892
1260
  const firstRoot = roots[0];
893
1261
  if (firstRoot.uri && firstRoot.uri.startsWith('file://')) {
894
- // Convert file:// URI to local path
895
- // Handle both Unix (/path) and Windows (C:/path) paths
896
- let rootPath = firstRoot.uri.replace('file://', '');
897
- // Windows: file:///C:/path -> C:/path
898
- rootPath = rootPath.replace(/^\/([A-Z]:)/i, '$1');
1262
+ const rootPath = this.fileUriToLocalPath(firstRoot.uri);
1263
+ if (!rootPath) {
1264
+ this.log(`⚠️ Could not parse root URI: ${firstRoot.uri}`);
1265
+ this.loadConfig();
1266
+ return;
1267
+ }
899
1268
  this.log(`✅ Got workspace root from client: ${rootPath} (${firstRoot.name || 'unnamed'})`);
900
1269
  this.workspaceRoot = rootPath;
901
1270
  // Now load config with the correct workspace root
@@ -913,6 +1282,47 @@ class FraimLocalMCPServer {
913
1282
  }
914
1283
  }
915
1284
  }
1285
+ /**
1286
+ * Convert a file:// URI to a local filesystem path.
1287
+ */
1288
+ fileUriToLocalPath(uri) {
1289
+ if (!uri.startsWith('file://'))
1290
+ return null;
1291
+ let pathname = '';
1292
+ let host = '';
1293
+ try {
1294
+ const parsed = new URL(uri);
1295
+ if (parsed.protocol !== 'file:')
1296
+ return null;
1297
+ pathname = parsed.pathname || '';
1298
+ host = parsed.host || '';
1299
+ }
1300
+ catch {
1301
+ const raw = uri.replace('file://', '');
1302
+ try {
1303
+ pathname = decodeURIComponent(raw);
1304
+ }
1305
+ catch {
1306
+ pathname = raw;
1307
+ }
1308
+ }
1309
+ try {
1310
+ pathname = decodeURIComponent(pathname);
1311
+ }
1312
+ catch {
1313
+ // Keep raw path if decoding fails
1314
+ }
1315
+ let localPath = pathname;
1316
+ if (host && host !== 'localhost') {
1317
+ localPath = `//${host}${localPath}`;
1318
+ }
1319
+ // Windows drive letters can arrive as /C:/... or /c:/... or decoded from /c%3A/...
1320
+ localPath = localPath.replace(/^\/([a-zA-Z]:)/, '$1');
1321
+ if (process.platform === 'win32') {
1322
+ localPath = localPath.replace(/\//g, '\\');
1323
+ }
1324
+ return localPath;
1325
+ }
916
1326
  /**
917
1327
  * Start STDIO server
918
1328
  */
@@ -974,20 +1384,31 @@ class FraimLocalMCPServer {
974
1384
  });
975
1385
  process.stdin.on('end', () => {
976
1386
  this.log('🛑 Stdin closed, shutting down...');
1387
+ this.emitFallbackSummary('stdin_end');
977
1388
  process.exit(0);
978
1389
  });
979
1390
  process.on('SIGTERM', () => {
980
1391
  this.log('🛑 SIGTERM received, shutting down...');
1392
+ this.emitFallbackSummary('sigterm');
981
1393
  process.exit(0);
982
1394
  });
983
1395
  process.on('SIGINT', () => {
984
1396
  this.log('🛑 SIGINT received, shutting down...');
1397
+ this.emitFallbackSummary('sigint');
985
1398
  process.exit(0);
986
1399
  });
987
1400
  this.log('✅ FRAIM Local MCP Server ready');
988
1401
  }
989
1402
  }
990
1403
  exports.FraimLocalMCPServer = FraimLocalMCPServer;
1404
+ FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
1405
+ '## Agent Resolution Needed',
1406
+ 'Replace all `agent.*` placeholders with the best value from repository context, user intent, and available project config.'
1407
+ ].join('\n');
1408
+ FraimLocalMCPServer.FALLBACK_ALERT_MARKER = 'PROXY_FALLBACK_ALERT';
1409
+ FraimLocalMCPServer.FALLBACK_SUMMARY_MARKER = 'PROXY_FALLBACK_SUMMARY';
1410
+ FraimLocalMCPServer.FALLBACK_HEADER = 'x-fraim-proxy-fallback-telemetry';
1411
+ FraimLocalMCPServer.SESSION_HEADER = 'x-fraim-session-id';
991
1412
  // Start server if run directly
992
1413
  if (require.main === module) {
993
1414
  const server = new FraimLocalMCPServer();