fraim-framework 2.0.76 → 2.0.78

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.
@@ -106,19 +106,49 @@ class FraimTemplateEngine {
106
106
  return null;
107
107
  }
108
108
  substitutePlatformActions(content) {
109
- const provider = (0, provider_utils_1.detectProvider)(this.repoInfo?.url);
110
- const templates = this.loadProviderTemplates(provider);
111
- if (!templates)
112
- return content;
109
+ const codeProvider = this.getCodeProvider();
110
+ const issueProvider = this.getIssueProvider();
111
+ const codeTemplates = this.loadProviderTemplates(codeProvider);
112
+ const issueTemplates = issueProvider ? this.loadProviderTemplates(issueProvider) : null;
113
113
  let result = content;
114
- for (const [action, template] of Object.entries(templates)) {
115
- const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
- const regex = new RegExp(`\\{\\{${FraimTemplateEngine.PROXY_ACTION_PREFIX}${escapedAction}\\}\\}`, 'g');
117
- const renderedTemplate = this.renderActionTemplate(template);
118
- result = result.replace(regex, renderedTemplate);
114
+ if (issueTemplates) {
115
+ for (const [action, template] of Object.entries(issueTemplates)) {
116
+ if (!FraimTemplateEngine.ISSUE_ACTIONS.has(action))
117
+ continue;
118
+ const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
119
+ const regex = new RegExp(`\\{\\{${FraimTemplateEngine.PROXY_ACTION_PREFIX}${escapedAction}\\}\\}`, 'g');
120
+ result = result.replace(regex, this.renderActionTemplate(template));
121
+ }
122
+ }
123
+ if (codeTemplates) {
124
+ for (const [action, template] of Object.entries(codeTemplates)) {
125
+ if (issueProvider && issueProvider !== codeProvider && FraimTemplateEngine.ISSUE_ACTIONS.has(action))
126
+ continue;
127
+ const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
128
+ const regex = new RegExp(`\\{\\{${FraimTemplateEngine.PROXY_ACTION_PREFIX}${escapedAction}\\}\\}`, 'g');
129
+ result = result.replace(regex, this.renderActionTemplate(template));
130
+ }
119
131
  }
120
132
  return result;
121
133
  }
134
+ getCodeProvider() {
135
+ const repoProvider = this.repoInfo?.provider || this.config?.repository?.provider;
136
+ if (typeof repoProvider === 'string' && repoProvider.trim().length > 0) {
137
+ return repoProvider;
138
+ }
139
+ return (0, provider_utils_1.detectProvider)(this.repoInfo?.url || this.config?.repository?.url);
140
+ }
141
+ getIssueProvider() {
142
+ const fromRepoInfo = this.repoInfo?.issueTracking?.provider;
143
+ if (typeof fromRepoInfo === 'string' && fromRepoInfo.trim().length > 0) {
144
+ return fromRepoInfo;
145
+ }
146
+ const fromConfig = this.config?.issueTracking?.provider;
147
+ if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
148
+ return fromConfig;
149
+ }
150
+ return null;
151
+ }
122
152
  loadProviderTemplates(provider) {
123
153
  if (this.providerTemplatesCache[provider])
124
154
  return this.providerTemplatesCache[provider];
@@ -130,11 +160,12 @@ class FraimTemplateEngine {
130
160
  return null;
131
161
  }
132
162
  renderActionTemplate(template) {
133
- if (!this.repoInfo && !this.config?.repository) {
163
+ if (!this.repoInfo && !this.config?.repository && !this.config?.issueTracking) {
134
164
  return template;
135
165
  }
136
166
  return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
137
167
  const trimmedPath = path.trim();
168
+ // Handle proxy.repository.* variables
138
169
  if (trimmedPath.startsWith('proxy.repository.')) {
139
170
  const repoPath = trimmedPath.substring('proxy.repository.'.length);
140
171
  if (this.repoInfo) {
@@ -148,12 +179,36 @@ class FraimTemplateEngine {
148
179
  return String(value);
149
180
  }
150
181
  }
182
+ // Handle proxy.issueTracking.* variables (for split provider mode)
183
+ if (trimmedPath.startsWith('proxy.issueTracking.')) {
184
+ const issueTrackingPath = trimmedPath.substring('proxy.issueTracking.'.length);
185
+ if (this.repoInfo?.issueTracking) {
186
+ const value = (0, object_utils_1.getNestedValue)(this.repoInfo.issueTracking, issueTrackingPath);
187
+ if (value !== undefined)
188
+ return String(value);
189
+ }
190
+ if (this.config?.issueTracking) {
191
+ const value = (0, object_utils_1.getNestedValue)(this.config.issueTracking, issueTrackingPath);
192
+ if (value !== undefined)
193
+ return String(value);
194
+ }
195
+ }
151
196
  return match;
152
197
  });
153
198
  }
154
199
  }
155
200
  exports.FraimTemplateEngine = FraimTemplateEngine;
156
201
  FraimTemplateEngine.PROXY_ACTION_PREFIX = 'proxy.action.';
202
+ FraimTemplateEngine.ISSUE_ACTIONS = new Set([
203
+ 'get_issue',
204
+ 'update_issue_status',
205
+ 'add_issue_comment',
206
+ 'create_issue',
207
+ 'assign_issue',
208
+ 'search_issues',
209
+ 'close_issue',
210
+ 'list_issues'
211
+ ]);
157
212
  class FraimLocalMCPServer {
158
213
  constructor() {
159
214
  this.config = null;
@@ -365,48 +420,71 @@ class FraimLocalMCPServer {
365
420
  }).trim();
366
421
  }
367
422
  catch (error) {
368
- // If git command fails, construct URL from config if available
369
- if (this.config?.repository?.owner && this.config?.repository?.name) {
370
- const provider = this.config.repository.provider || 'github';
371
- if (provider === 'github') {
372
- repoUrl = `https://github.com/${this.config.repository.owner}/${this.config.repository.name}.git`;
373
- }
374
- else if (provider === 'ado') {
375
- // Azure DevOps URL format
376
- repoUrl = `https://dev.azure.com/${this.config.repository.owner}/${this.config.repository.name}/_git/${this.config.repository.name}`;
423
+ // If git command fails, construct URL from config if available.
424
+ const repositoryConfig = this.config?.repository;
425
+ if (repositoryConfig?.url) {
426
+ repoUrl = repositoryConfig.url;
427
+ this.log(`Constructed repo URL from config: ${repoUrl}`);
428
+ }
429
+ else if (repositoryConfig?.provider === 'github' && repositoryConfig.owner && repositoryConfig.name) {
430
+ repoUrl = `https://github.com/${repositoryConfig.owner}/${repositoryConfig.name}.git`;
431
+ this.log(`Constructed repo URL from config: ${repoUrl}`);
432
+ }
433
+ else if (repositoryConfig?.provider === 'ado' &&
434
+ repositoryConfig.organization &&
435
+ repositoryConfig.project &&
436
+ repositoryConfig.name) {
437
+ repoUrl = `https://dev.azure.com/${repositoryConfig.organization}/${repositoryConfig.project}/_git/${repositoryConfig.name}`;
438
+ this.log(`Constructed repo URL from config: ${repoUrl}`);
439
+ }
440
+ else if (repositoryConfig?.provider === 'gitlab') {
441
+ const projectPath = repositoryConfig.projectPath
442
+ || (repositoryConfig.namespace && repositoryConfig.name
443
+ ? `${repositoryConfig.namespace}/${repositoryConfig.name}`
444
+ : null);
445
+ if (projectPath) {
446
+ repoUrl = `https://gitlab.com/${projectPath}.git`;
447
+ this.log(`Constructed repo URL from config: ${repoUrl}`);
377
448
  }
378
- this.log(`📋 Constructed repo URL from config: ${repoUrl}`);
379
449
  }
380
450
  }
381
451
  if (!repoUrl) {
382
- this.log('⚠️ No git repository found and no config available');
452
+ this.log('No git repository found and no config available');
383
453
  return null;
384
454
  }
385
- // Parse owner and name from URL
386
- let owner = '';
455
+ // Parse repository identity from URL
387
456
  let name = '';
457
+ let owner = '';
388
458
  let organization = '';
389
459
  let project = '';
390
- // Handle GitHub URLs: https://github.com/owner/repo.git or git@github.com:owner/repo.git
391
- const githubHttpsMatch = repoUrl.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/);
392
- const adoMatch = repoUrl.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/]+)/);
393
- if (githubHttpsMatch) {
394
- owner = githubHttpsMatch[1];
395
- name = githubHttpsMatch[2];
460
+ let namespace = '';
461
+ let projectPath = '';
462
+ const githubMatch = repoUrl.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/i);
463
+ const adoMatch = repoUrl.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/]+)/i);
464
+ const gitlabMatch = repoUrl.match(/gitlab[^\/:]*[\/:]([^?\s#]+?)(?:\.git)?$/i);
465
+ if (githubMatch) {
466
+ owner = githubMatch[1];
467
+ name = githubMatch[2];
396
468
  }
397
469
  else if (adoMatch) {
398
- // Azure DevOps: organization and project are separate fields
399
470
  organization = adoMatch[1];
400
471
  project = adoMatch[2];
401
- owner = organization; // For compatibility
402
472
  name = adoMatch[3];
403
473
  }
474
+ else if (gitlabMatch) {
475
+ projectPath = gitlabMatch[1].replace(/^\/+/, '');
476
+ const segments = projectPath.split('/').filter(Boolean);
477
+ name = segments[segments.length - 1] || '';
478
+ namespace = segments.slice(0, -1).join('/');
479
+ }
404
480
  else if (this.config?.repository) {
405
481
  // Fall back to config if URL parsing fails
406
482
  owner = this.config.repository.owner || '';
407
483
  name = this.config.repository.name || '';
408
484
  organization = this.config.repository.organization || '';
409
485
  project = this.config.repository.project || '';
486
+ namespace = this.config.repository.namespace || '';
487
+ projectPath = this.config.repository.projectPath || (namespace && name ? `${namespace}/${name}` : '');
410
488
  }
411
489
  // Get current branch
412
490
  let branch = '';
@@ -423,15 +501,23 @@ class FraimLocalMCPServer {
423
501
  branch = this.config.repository.defaultBranch;
424
502
  }
425
503
  }
426
- this.repoInfo = {
504
+ const repoInfo = {
427
505
  url: repoUrl,
428
- owner: owner || 'unknown',
429
506
  name: name || 'unknown',
430
507
  ...(organization && { organization }),
431
508
  ...(project && { project }),
509
+ ...(namespace && { namespace }),
510
+ ...(projectPath && { projectPath }),
432
511
  ...(branch && { branch })
433
512
  };
434
- this.log(`✅ Detected repo info: ${this.repoInfo.owner}/${this.repoInfo.name}`);
513
+ if (owner) {
514
+ repoInfo.owner = owner;
515
+ }
516
+ this.repoInfo = repoInfo;
517
+ const repoLabel = this.repoInfo.owner
518
+ ? `${this.repoInfo.owner}/${this.repoInfo.name}`
519
+ : this.repoInfo.projectPath || this.repoInfo.name;
520
+ this.log(`Detected repo info: ${repoLabel}`);
435
521
  return this.repoInfo;
436
522
  }
437
523
  catch (error) {
@@ -630,25 +716,57 @@ class FraimLocalMCPServer {
630
716
  if (!hasDirectProviderPlaceholders && !hasDeliveryPlaceholders)
631
717
  return;
632
718
  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;
719
+ const codeProvider = engine.getCodeProvider();
720
+ const issueProvider = engine.getIssueProvider();
721
+ const providers = Array.from(new Set([codeProvider, issueProvider].filter((provider) => typeof provider === 'string' && provider.length > 0)));
722
+ for (const provider of providers) {
723
+ if (engine.hasProviderTemplates(provider))
724
+ continue;
725
+ if (engine.loadProviderTemplates(provider))
726
+ continue;
727
+ const filename = `${provider}.json`;
728
+ const cached = this.readCachedTemplateFile(filename);
729
+ if (cached) {
730
+ engine.setProviderTemplates(provider, cached);
731
+ continue;
732
+ }
733
+ const remoteContent = await this.fetchRegistryFileFromServer(`providers/${filename}`, requestSessionId);
734
+ if (remoteContent) {
735
+ const parsed = this.parseTemplateJson(remoteContent, `providers/${filename}`);
736
+ if (parsed) {
737
+ engine.setProviderTemplates(provider, parsed);
738
+ this.writeCachedTemplateFile(filename, parsed);
739
+ continue;
740
+ }
741
+ }
742
+ const bundled = this.readBundledTemplateFile(filename);
743
+ if (bundled) {
744
+ engine.setProviderTemplates(provider, bundled);
745
+ }
643
746
  }
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);
747
+ }
748
+ readBundledTemplateFile(filename) {
749
+ const candidates = [
750
+ (0, path_1.join)(__dirname, '..', '..', '..', 'registry', 'providers', filename),
751
+ (0, path_1.join)(__dirname, '..', '..', 'registry', 'providers', filename),
752
+ (0, path_1.join)(process.cwd(), 'registry', 'providers', filename)
753
+ ];
754
+ for (const candidate of candidates) {
755
+ try {
756
+ if (!(0, fs_1.existsSync)(candidate))
757
+ continue;
758
+ const content = (0, fs_1.readFileSync)(candidate, 'utf8');
759
+ const parsed = JSON.parse(content);
760
+ if (parsed && typeof parsed === 'object') {
761
+ this.log(`✅ Loaded bundled provider template: ${candidate}`);
762
+ return parsed;
763
+ }
764
+ }
765
+ catch (error) {
766
+ this.log(`⚠️ Failed to load bundled template ${candidate}: ${error.message}`);
767
+ }
768
+ }
769
+ return null;
652
770
  }
653
771
  async hydrateTemplateCachesForResponse(response, requestSessionId) {
654
772
  if (!response.result)
@@ -661,10 +779,12 @@ class FraimLocalMCPServer {
661
779
  let processedResponse = this.processResponse(response);
662
780
  // Delivery substitution can introduce provider action placeholders (e.g. {{proxy.action.update_issue_status}})
663
781
  // 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
782
  const engine = this.ensureEngine();
666
- if (this.responseHasPotentialProviderPlaceholders(processedResponse) &&
667
- !engine.hasProviderTemplates(provider)) {
783
+ const codeProvider = engine.getCodeProvider();
784
+ const issueProvider = engine.getIssueProvider();
785
+ const providers = Array.from(new Set([codeProvider, issueProvider].filter((provider) => typeof provider === 'string' && provider.length > 0)));
786
+ const hasMissingProviderTemplate = providers.some((provider) => !engine.hasProviderTemplates(provider));
787
+ if (this.responseHasPotentialProviderPlaceholders(processedResponse) && hasMissingProviderTemplate) {
668
788
  await this.ensureProviderTemplatesAvailable(processedResponse, requestSessionId);
669
789
  processedResponse = this.processResponse(processedResponse);
670
790
  }
@@ -960,6 +1080,44 @@ class FraimLocalMCPServer {
960
1080
  request.params.arguments.sessionId = requestSessionId;
961
1081
  }
962
1082
  }
1083
+ normalizeRepoContext(repo) {
1084
+ if (!repo || typeof repo !== 'object')
1085
+ return repo;
1086
+ const normalized = { ...repo };
1087
+ const detectedProvider = (0, provider_utils_1.detectProvider)(normalized.url);
1088
+ if (typeof detectedProvider === 'string' && detectedProvider.length > 0) {
1089
+ normalized.provider = detectedProvider;
1090
+ }
1091
+ if (normalized.provider !== 'gitlab')
1092
+ return normalized;
1093
+ if (!normalized.projectPath && typeof normalized.url === 'string') {
1094
+ const gitlabMatch = normalized.url.match(/gitlab[^\/:]*[\/:]([^?\s#]+?)(?:\.git)?$/i);
1095
+ if (gitlabMatch) {
1096
+ normalized.projectPath = gitlabMatch[1].replace(/^\/+/, '');
1097
+ }
1098
+ }
1099
+ if (!normalized.name && typeof normalized.projectPath === 'string') {
1100
+ const segments = normalized.projectPath.split('/').filter(Boolean);
1101
+ normalized.name = segments[segments.length - 1] || normalized.name;
1102
+ normalized.namespace = normalized.namespace || segments.slice(0, -1).join('/');
1103
+ }
1104
+ if (normalized.projectPath || !normalized.namespace || !normalized.name)
1105
+ return normalized;
1106
+ normalized.projectPath = `${normalized.namespace}/${normalized.name}`;
1107
+ return normalized;
1108
+ }
1109
+ normalizeIssueTrackingContext(issueTracking) {
1110
+ if (!issueTracking || typeof issueTracking !== 'object')
1111
+ return issueTracking;
1112
+ if (issueTracking.provider !== 'gitlab')
1113
+ return issueTracking;
1114
+ if (issueTracking.projectPath || !issueTracking.namespace || !issueTracking.name)
1115
+ return issueTracking;
1116
+ return {
1117
+ ...issueTracking,
1118
+ projectPath: `${issueTracking.namespace}/${issueTracking.name}`
1119
+ };
1120
+ }
963
1121
  /**
964
1122
  * Proxy request to remote FRAIM server
965
1123
  */
@@ -988,7 +1146,9 @@ class FraimLocalMCPServer {
988
1146
  ...args.repo, // Agent values as fallback
989
1147
  ...detectedRepo // Detected values override (always win)
990
1148
  };
991
- this.log(`[req:${requestId}] Auto-detected and injected repo info: ${args.repo.owner}/${args.repo.name}`);
1149
+ args.repo = this.normalizeRepoContext(args.repo);
1150
+ const repoLabel = args.repo.owner ? `${args.repo.owner}/${args.repo.name}` : args.repo.name;
1151
+ this.log(`[req:${requestId}] Auto-detected and injected repo info: ${repoLabel}`);
992
1152
  }
993
1153
  else {
994
1154
  // If detection fails, use agent-provided values (if any)
@@ -1004,12 +1164,30 @@ class FraimLocalMCPServer {
1004
1164
  }
1005
1165
  };
1006
1166
  }
1007
- this.log(`[req:${requestId}] Using agent-provided repo info: ${args.repo.owner}/${args.repo.name}`);
1167
+ args.repo = this.normalizeRepoContext(args.repo);
1168
+ const repoLabel = args.repo.owner ? `${args.repo.owner}/${args.repo.name}` : args.repo.name;
1169
+ this.log(`[req:${requestId}] Using agent-provided repo info: ${repoLabel}`);
1008
1170
  }
1171
+ const configuredIssueTracking = this.config?.issueTracking;
1172
+ if (configuredIssueTracking && typeof configuredIssueTracking === 'object') {
1173
+ args.issueTracking = {
1174
+ ...(args.issueTracking || {}),
1175
+ ...configuredIssueTracking
1176
+ };
1177
+ args.issueTracking = this.normalizeIssueTrackingContext(args.issueTracking);
1178
+ this.log(`[req:${requestId}] Applied issueTracking context: ${args.issueTracking.provider || 'unknown'}`);
1179
+ }
1180
+ else if (args.issueTracking) {
1181
+ args.issueTracking = this.normalizeIssueTrackingContext(args.issueTracking);
1182
+ }
1183
+ const runtimeRepoContext = {
1184
+ ...args.repo,
1185
+ ...(args.issueTracking ? { issueTracking: args.issueTracking } : {})
1186
+ };
1009
1187
  // Keep proxy runtime repository context in sync with connect payload.
1010
- this.repoInfo = args.repo;
1188
+ this.repoInfo = runtimeRepoContext;
1011
1189
  if (this.engine) {
1012
- this.engine.setRepoInfo(args.repo);
1190
+ this.engine.setRepoInfo(runtimeRepoContext);
1013
1191
  }
1014
1192
  // Update the request with injected info
1015
1193
  request.params.arguments = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.76",
3
+ "version": "2.0.78",
4
4
  "description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
5
5
  "main": "index.js",
6
6
  "bin": {