fraim-framework 2.0.90 → 2.0.92

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/bin/fraim.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework CLI Entry Point
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mcpCommand = void 0;
4
+ const commander_1 = require("commander");
5
+ const stdio_server_1 = require("../../local-mcp-server/stdio-server");
6
+ /**
7
+ * mcpCommand - Entry point for starting the local FRAIM MCP server.
8
+ * This integrates the local-mcp-server into the main CLI.
9
+ */
10
+ exports.mcpCommand = new commander_1.Command('mcp')
11
+ .description('Start the local FRAIM MCP server (stdio)')
12
+ .action(() => {
13
+ const server = new stdio_server_1.FraimLocalMCPServer();
14
+ server.start();
15
+ });
@@ -48,6 +48,7 @@ const add_provider_1 = require("./commands/add-provider");
48
48
  const override_1 = require("./commands/override");
49
49
  const list_overridable_1 = require("./commands/list-overridable");
50
50
  const login_1 = require("./commands/login");
51
+ const mcp_1 = require("./commands/mcp");
51
52
  const fs_1 = __importDefault(require("fs"));
52
53
  const path_1 = __importDefault(require("path"));
53
54
  const program = new commander_1.Command();
@@ -85,6 +86,7 @@ program.addCommand(add_provider_1.addProviderCommand);
85
86
  program.addCommand(override_1.overrideCommand);
86
87
  program.addCommand(list_overridable_1.listOverridableCommand);
87
88
  program.addCommand(login_1.loginCommand);
89
+ program.addCommand(mcp_1.mcpCommand);
88
90
  // Wait for async command initialization before parsing
89
91
  (async () => {
90
92
  // Import the initialization promise from setup command
@@ -36,7 +36,8 @@ exports.BASE_MCP_SERVERS = [
36
36
  name: 'FRAIM',
37
37
  description: 'FRAIM workflow orchestration and mentoring',
38
38
  buildServer: (fraimKey) => ({
39
- command: 'fraim-mcp',
39
+ command: 'npx',
40
+ args: ['-y', 'fraim-framework@latest', 'mcp'],
40
41
  env: {
41
42
  // Include API key for IDE configs (Codex, VSCode, etc.)
42
43
  // The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DeviceFlowService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class DeviceFlowService {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ /**
14
+ * Start the Device Flow Login
15
+ */
16
+ async login() {
17
+ console.log(chalk_1.default.blue('\nšŸ”— Starting Authentication...'));
18
+ try {
19
+ // 1. Request device and user codes
20
+ const deviceCode = await this.requestDeviceCode();
21
+ console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
22
+ console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
23
+ console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
24
+ console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
25
+ // 2. Poll for the access token
26
+ const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
27
+ console.log(chalk_1.default.green('\nāœ… Authentication Successful!'));
28
+ return token;
29
+ }
30
+ catch (error) {
31
+ console.error(chalk_1.default.red(`\nāŒ Authentication failed: ${error.message}`));
32
+ throw error;
33
+ }
34
+ }
35
+ async requestDeviceCode() {
36
+ const response = await axios_1.default.post(this.config.authUrl, {
37
+ client_id: this.config.clientId,
38
+ scope: this.config.scope
39
+ }, {
40
+ headers: { Accept: 'application/json' }
41
+ });
42
+ return response.data;
43
+ }
44
+ async pollForToken(deviceCode, interval) {
45
+ let currentInterval = interval * 1000;
46
+ return new Promise((resolve, reject) => {
47
+ const poll = async () => {
48
+ try {
49
+ const response = await axios_1.default.post(this.config.tokenUrl, {
50
+ client_id: this.config.clientId,
51
+ device_code: deviceCode,
52
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
53
+ }, {
54
+ headers: { Accept: 'application/json' }
55
+ });
56
+ if (response.data.access_token) {
57
+ resolve(response.data.access_token);
58
+ return;
59
+ }
60
+ if (response.data.error) {
61
+ const error = response.data.error;
62
+ if (error === 'authorization_pending') {
63
+ // Keep polling
64
+ setTimeout(poll, currentInterval);
65
+ }
66
+ else if (error === 'slow_down') {
67
+ currentInterval += 5000;
68
+ setTimeout(poll, currentInterval);
69
+ }
70
+ else {
71
+ reject(new Error(response.data.error_description || error));
72
+ }
73
+ }
74
+ }
75
+ catch (error) {
76
+ reject(error);
77
+ }
78
+ };
79
+ setTimeout(poll, currentInterval);
80
+ });
81
+ }
82
+ }
83
+ exports.DeviceFlowService = DeviceFlowService;
@@ -46,6 +46,11 @@ function loadFraimConfig() {
46
46
  ...(config.customizations || {})
47
47
  }
48
48
  };
49
+ if (config.customizations?.postCleanupHook || config.customizations?.cleanupCommand) {
50
+ if (!mergedConfig.customizations)
51
+ mergedConfig.customizations = {};
52
+ mergedConfig.customizations.postCleanupHook = config.customizations.postCleanupHook || config.customizations.cleanupCommand;
53
+ }
49
54
  if (config.issueTracking && typeof config.issueTracking === 'object') {
50
55
  mergedConfig.issueTracking = config.issueTracking;
51
56
  }
@@ -2,6 +2,8 @@
2
2
  /**
3
3
  * FRAIM Configuration Types
4
4
  * TypeScript types for .fraim/config.json
5
+ *
6
+ * Each field includes rich metadata for agent detection and user interaction
5
7
  */
6
8
  Object.defineProperty(exports, "__esModule", { value: true });
7
9
  exports.DEFAULT_FRAIM_CONFIG = void 0;
@@ -31,7 +31,7 @@ function getPort() {
31
31
  */
32
32
  function determineDatabaseName() {
33
33
  try {
34
- const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
34
+ const branchName = process.env.FRAIM_BRANCH || process.env.FRAIM_BRANCH_NAME || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
35
35
  const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
36
36
  if (issueMatch) {
37
37
  return `fraim_issue_${issueMatch[1]}`;
@@ -50,11 +50,11 @@ function extractLeadParagraph(content) {
50
50
  * These stubs are committed to the user's repo for discoverability.
51
51
  */
52
52
  function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
53
- return `${STUB_MARKER}
54
- # FRAIM Workflow: ${workflowName}
55
-
56
- > [!IMPORTANT]
57
- > This is a **FRAIM-managed workflow stub**.
53
+ return `${STUB_MARKER}
54
+ # FRAIM Workflow: ${workflowName}
55
+
56
+ > [!IMPORTANT]
57
+ > This is a **FRAIM-managed workflow stub**.
58
58
  > To load the full context (rules, templates, and execution steps), ask your AI agent to:
59
59
  > \`@fraim get_fraim_workflow("${workflowName}")\`
60
60
  >
@@ -81,19 +81,19 @@ function parseRegistryWorkflow(content) {
81
81
  * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
82
82
  */
83
83
  function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
84
- return `${STUB_MARKER}
85
- # FRAIM Job: ${jobName}
86
-
87
- ## Intent
88
- ${intent}
89
-
90
- ## Outcome
91
- ${outcome}
92
-
93
- ## Steps
94
- ${steps}
95
-
96
- ---
84
+ return `${STUB_MARKER}
85
+ # FRAIM Job: ${jobName}
86
+
87
+ ## Intent
88
+ ${intent}
89
+
90
+ ## Outcome
91
+ ${outcome}
92
+
93
+ ## Steps
94
+ ${steps}
95
+
96
+ ---
97
97
 
98
98
  > [!IMPORTANT]
99
99
  > **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
@@ -110,41 +110,41 @@ ${steps}
110
110
  * Generates a lightweight markdown stub for a skill.
111
111
  */
112
112
  function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
113
- return `${STUB_MARKER}
114
- # FRAIM Skill: ${skillName}
115
-
116
- ## Skill Input
117
- ${skillInput}
118
-
119
- ## Skill Output
120
- ${skillOutput}
121
-
122
- ---
123
-
124
- > [!IMPORTANT]
125
- > **For AI Agents:** This is a discoverability stub for the skill.
126
- > All execution details must be fetched from MCP before use.
127
- > To retrieve the complete skill instructions, call:
128
- > \`get_fraim_file({ path: "skills/${skillPath}" })\`
113
+ return `${STUB_MARKER}
114
+ # FRAIM Skill: ${skillName}
115
+
116
+ ## Skill Input
117
+ ${skillInput}
118
+
119
+ ## Skill Output
120
+ ${skillOutput}
121
+
122
+ ---
123
+
124
+ > [!IMPORTANT]
125
+ > **For AI Agents:** This is a discoverability stub for the skill.
126
+ > All execution details must be fetched from MCP before use.
127
+ > To retrieve the complete skill instructions, call:
128
+ > \`get_fraim_file({ path: "skills/${skillPath}" })\`
129
129
  `;
130
130
  }
131
131
  /**
132
132
  * Generates a lightweight markdown stub for a rule.
133
133
  */
134
134
  function generateRuleStub(ruleName, rulePath, intent) {
135
- return `${STUB_MARKER}
136
- # FRAIM Rule: ${ruleName}
137
-
138
- ## Intent
139
- ${intent}
140
-
141
- ---
142
-
143
- > [!IMPORTANT]
144
- > **For AI Agents:** This is a discoverability stub for the rule.
145
- > All rule details must be fetched from MCP before use.
146
- > To retrieve the complete rule instructions, call:
147
- > \`get_fraim_file({ path: "rules/${rulePath}" })\`
135
+ return `${STUB_MARKER}
136
+ # FRAIM Rule: ${ruleName}
137
+
138
+ ## Intent
139
+ ${intent}
140
+
141
+ ---
142
+
143
+ > [!IMPORTANT]
144
+ > **For AI Agents:** This is a discoverability stub for the rule.
145
+ > All rule details must be fetched from MCP before use.
146
+ > To retrieve the complete rule instructions, call:
147
+ > \`get_fraim_file({ path: "rules/${rulePath}" })\`
148
148
  `;
149
149
  }
150
150
  /**
@@ -29,6 +29,7 @@ const provider_utils_1 = require("../core/utils/provider-utils");
29
29
  const object_utils_1 = require("../core/utils/object-utils");
30
30
  const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
31
31
  const ai_mentor_1 = require("../core/ai-mentor");
32
+ const usage_collector_js_1 = require("./usage-collector.js");
32
33
  /**
33
34
  * Handle template substitution logic separately for better testability
34
35
  */
@@ -264,6 +265,9 @@ class FraimLocalMCPServer {
264
265
  this.log(`šŸ”‘ API key: ${this.apiKey.substring(0, 10)}...`);
265
266
  this.log(`Local MCP version: ${this.localVersion}`);
266
267
  this.log(`šŸ” DEBUG BUILD: Machine detection v2 active`);
268
+ // Initialize usage collector
269
+ this.usageCollector = new usage_collector_js_1.UsageCollector();
270
+ this.log('šŸ“Š Usage analytics collector initialized');
267
271
  }
268
272
  /**
269
273
  * Load API key from environment variable or user config file
@@ -1260,6 +1264,8 @@ class FraimLocalMCPServer {
1260
1264
  this.engine.setMachineInfo(this.machineInfo);
1261
1265
  this.engine.setRepoInfo(this.repoInfo);
1262
1266
  }
1267
+ // In a proxy setup, the remote server resolves the API key ID during event upload.
1268
+ // No local resolution needed.
1263
1269
  // Update the request with injected info
1264
1270
  request.params.arguments = args;
1265
1271
  }
@@ -1402,9 +1408,9 @@ class FraimLocalMCPServer {
1402
1408
  // Force ALL tools/call requests to return raw definitions so the proxy
1403
1409
  // can resolve templates and replace includes locally
1404
1410
  const toolName = request.params?.name;
1411
+ const args = request.params?.arguments || {};
1405
1412
  let injectedRequest = request;
1406
1413
  if (request.method === 'tools/call' && typeof toolName === 'string') {
1407
- const args = request.params.arguments || {};
1408
1414
  // šŸ” SMART DISPATCHER: Intercept mentoring and job/workflow tools for local overrides
1409
1415
  if (toolName === 'seekMentoring') {
1410
1416
  try {
@@ -1482,6 +1488,11 @@ class FraimLocalMCPServer {
1482
1488
  }
1483
1489
  const response = await this._doProxyToRemote(injectedRequest, requestId);
1484
1490
  const processedResponse = await this.finalizeToolResponse(injectedRequest, response, requestSessionId, requestId);
1491
+ // Single point for usage tracking - log all tool calls
1492
+ if (injectedRequest.method === 'tools/call' && requestSessionId && toolName) {
1493
+ const success = !processedResponse.error;
1494
+ this.usageCollector.collectMCPCall(toolName, args, requestSessionId, success);
1495
+ }
1485
1496
  this.log(`šŸ“¤ ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1486
1497
  return processedResponse;
1487
1498
  }
@@ -1574,6 +1585,17 @@ class FraimLocalMCPServer {
1574
1585
  start() {
1575
1586
  let buffer = '';
1576
1587
  process.stdin.setEncoding('utf8');
1588
+ // Set up periodic usage data upload
1589
+ const uploadInterval = setInterval(() => {
1590
+ this.uploadUsageData().catch(error => {
1591
+ this.log(`āš ļø Failed to upload usage data: ${error.message}`);
1592
+ });
1593
+ }, 60000); // Upload every minute
1594
+ // Clean up interval on shutdown
1595
+ const cleanup = () => {
1596
+ clearInterval(uploadInterval);
1597
+ this.usageCollector.shutdown();
1598
+ };
1577
1599
  process.stdin.on('data', async (chunk) => {
1578
1600
  buffer += chunk;
1579
1601
  // Process complete JSON-RPC messages (newline-delimited)
@@ -1629,21 +1651,40 @@ class FraimLocalMCPServer {
1629
1651
  });
1630
1652
  process.stdin.on('end', () => {
1631
1653
  this.log('šŸ›‘ Stdin closed, shutting down...');
1654
+ cleanup();
1632
1655
  this.emitFallbackSummary('stdin_end');
1633
1656
  process.exit(0);
1634
1657
  });
1635
1658
  process.on('SIGTERM', () => {
1636
1659
  this.log('šŸ›‘ SIGTERM received, shutting down...');
1660
+ cleanup();
1637
1661
  this.emitFallbackSummary('sigterm');
1638
1662
  process.exit(0);
1639
1663
  });
1640
1664
  process.on('SIGINT', () => {
1641
1665
  this.log('šŸ›‘ SIGINT received, shutting down...');
1666
+ cleanup();
1642
1667
  this.emitFallbackSummary('sigint');
1643
1668
  process.exit(0);
1644
1669
  });
1645
1670
  this.log('āœ… FRAIM Local MCP Server ready');
1646
1671
  }
1672
+ /**
1673
+ * Flush collected usage data to the local database
1674
+ */
1675
+ async uploadUsageData() {
1676
+ if (this.usageCollector.getEventCount() === 0) {
1677
+ return; // Nothing to flush
1678
+ }
1679
+ try {
1680
+ const count = this.usageCollector.getEventCount();
1681
+ await this.usageCollector.flush(this.remoteUrl, this.apiKey);
1682
+ this.log(`šŸ“Š Flushed ${count} usage events to remote server`);
1683
+ }
1684
+ catch (error) {
1685
+ this.log(`āŒ Usage flushing error: ${error.message}`);
1686
+ }
1687
+ }
1647
1688
  }
1648
1689
  exports.FraimLocalMCPServer = FraimLocalMCPServer;
1649
1690
  FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UsageCollector = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ /**
9
+ * Usage event collector for local MCP proxy
10
+ * Collects usage events and batches them for upload
11
+ */
12
+ class UsageCollector {
13
+ constructor() {
14
+ this.events = [];
15
+ this.apiKeyId = null;
16
+ }
17
+ /**
18
+ * Set the API key ID for this session
19
+ */
20
+ setApiKeyId(apiKeyId) {
21
+ this.apiKeyId = apiKeyId;
22
+ }
23
+ /**
24
+ * Collect MCP tool call event
25
+ */
26
+ collectMCPCall(toolName, args, sessionId, success = true, duration) {
27
+ if (!this.apiKeyId) {
28
+ return; // Silently skip if no API key
29
+ }
30
+ const parsed = this.parseMCPCall(toolName, args);
31
+ if (!parsed)
32
+ return;
33
+ const event = {
34
+ type: parsed.type,
35
+ name: parsed.name,
36
+ apiKeyId: this.apiKeyId,
37
+ sessionId,
38
+ success
39
+ };
40
+ if (duration !== undefined) {
41
+ event.duration = duration;
42
+ }
43
+ this.events.push(event);
44
+ }
45
+ /**
46
+ * Collect usage event directly (for backward compatibility with tests)
47
+ */
48
+ collectEvent(type, name, sessionId, success = true, duration) {
49
+ if (!this.apiKeyId) {
50
+ return; // Silently skip if no API key
51
+ }
52
+ const event = {
53
+ type,
54
+ name,
55
+ apiKeyId: this.apiKeyId,
56
+ sessionId,
57
+ success
58
+ };
59
+ if (duration !== undefined) {
60
+ event.duration = duration;
61
+ }
62
+ this.events.push(event);
63
+ }
64
+ /**
65
+ * Parse MCP tool call to extract usage analytics info
66
+ */
67
+ parseMCPCall(toolName, args) {
68
+ switch (toolName) {
69
+ case 'get_fraim_job':
70
+ return { type: 'job', name: args.job || 'unknown' };
71
+ case 'get_fraim_file':
72
+ if (args.path) {
73
+ return UsageCollector.parseComponentName(args.path);
74
+ }
75
+ return null;
76
+ case 'seekMentoring':
77
+ return { type: 'mentoring', name: args.workflowType || 'unknown' };
78
+ default:
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Get collected events for upload and clear the queue
84
+ */
85
+ getEventsForUpload() {
86
+ const eventsToUpload = [...this.events];
87
+ this.events = [];
88
+ return eventsToUpload;
89
+ }
90
+ /**
91
+ * Get current event count
92
+ */
93
+ getEventCount() {
94
+ return this.events.length;
95
+ }
96
+ /**
97
+ * Flush events to the remote server via API
98
+ */
99
+ async flush(remoteUrl, apiKey) {
100
+ if (this.events.length === 0)
101
+ return;
102
+ const events = [...this.events];
103
+ this.events = [];
104
+ try {
105
+ await axios_1.default.post(`${remoteUrl}/api/analytics/events`, {
106
+ events,
107
+ apiKey
108
+ }, {
109
+ timeout: 5000,
110
+ headers: {
111
+ 'x-api-key': apiKey,
112
+ 'Content-Type': 'application/json'
113
+ }
114
+ });
115
+ // Success - events are already cleared from the queue
116
+ }
117
+ catch (error) {
118
+ const status = error.response?.status;
119
+ const message = error.response?.data?.error || error.message;
120
+ console.error(`āŒ Failed to flush usage events (HTTP ${status}): ${message}`);
121
+ // Put events back at the beginning of the queue for next try
122
+ this.events = [...events, ...this.events];
123
+ }
124
+ }
125
+ /**
126
+ * Shutdown the collector
127
+ */
128
+ shutdown() {
129
+ this.events = [];
130
+ }
131
+ /**
132
+ * Parse component name from file path
133
+ */
134
+ static parseComponentName(path) {
135
+ if (path.includes('/jobs/')) {
136
+ const jobMatch = path.match(/\/jobs\/[^/]+\/[^/]+\/([^/]+)\.md$/);
137
+ if (jobMatch) {
138
+ return { type: 'job', name: jobMatch[1] };
139
+ }
140
+ }
141
+ if (path.includes('/skills/')) {
142
+ const skillMatch = path.match(/\/skills\/[^/]+\/([^/]+)\.md$/);
143
+ if (skillMatch) {
144
+ return { type: 'skill', name: skillMatch[1] };
145
+ }
146
+ }
147
+ if (path.includes('/rules/')) {
148
+ const ruleMatch = path.match(/\/rules\/[^/]+\/([^/]+)\.md$/);
149
+ if (ruleMatch) {
150
+ return { type: 'rule', name: ruleMatch[1] };
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ }
156
+ exports.UsageCollector = UsageCollector;
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.90",
3
+ "version": "2.0.92",
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": {
7
7
  "fraim": "./index.js",
8
- "fraim-framework": "./index.js",
9
- "fraim-mcp": "./bin/fraim-mcp.js"
8
+ "fraim-framework": "./index.js"
10
9
  },
11
10
  "scripts": {
12
11
  "dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
13
12
  "dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
14
- "build": "tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
13
+ "build": "tsc && npm run build:stubs && npm run build:fraim-brain && npm run build:config-schema && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
15
14
  "build:stubs": "tsx scripts/build-stub-registry.ts",
16
15
  "build:fraim-brain": "node scripts/generate-fraim-brain.js",
16
+ "build:config-schema": "tsx scripts/generate-config-schema-template.ts",
17
17
  "test-all": "npm run test && npm run test:isolated && npm run test:ui",
18
18
  "test": "node scripts/test-with-server.js",
19
19
  "test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
@@ -36,7 +36,7 @@
36
36
  "postinstall": "fraim sync --skip-updates || echo 'FRAIM setup skipped.'",
37
37
  "prepublishOnly": "npm run build",
38
38
  "release": "npm version patch && npm publish",
39
- "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping",
39
+ "validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens && npm run validate:brain-mapping && npm run validate:template-syntax",
40
40
  "validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
41
41
  "validate:fraim-pro-assets": "tsx scripts/validate-fraim-pro-assets.ts",
42
42
  "validate:workflows": "tsx scripts/validate-workflows.ts",
@@ -47,7 +47,8 @@
47
47
  "validate:bootstrap-config-coverage": "tsx scripts/validate-bootstrap-config-coverage.ts",
48
48
  "validate:provider-action-mappings": "tsx scripts/validate-provider-action-mappings.ts",
49
49
  "validate:fidelity": "tsx scripts/validate-fidelity.ts",
50
- "validate:config-tokens": "tsx scripts/validate-config-tokens.ts"
50
+ "validate:config-tokens": "tsx scripts/validate-config-tokens.ts",
51
+ "validate:template-syntax": "tsx scripts/validate-template-syntax.ts"
51
52
  },
52
53
  "repository": {
53
54
  "type": "git",
@@ -82,10 +83,12 @@
82
83
  },
83
84
  "devDependencies": {
84
85
  "@playwright/test": "^1.58.2",
86
+ "@types/adm-zip": "^0.5.7",
85
87
  "@types/cors": "^2.8.19",
86
88
  "@types/express": "^5.0.6",
87
89
  "@types/node": "^20.0.0",
88
90
  "@types/prompts": "^2.4.9",
91
+ "@types/semver": "^7.7.1",
89
92
  "fast-glob": "^3.3.3",
90
93
  "html-to-docx": "^1.8.0",
91
94
  "markdown-it": "^14.1.1",
@@ -114,6 +117,7 @@
114
117
  "access": "public"
115
118
  },
116
119
  "dependencies": {
120
+ "adm-zip": "^0.5.16",
117
121
  "axios": "^1.7.0",
118
122
  "chalk": "4.1.2",
119
123
  "commander": "^14.0.2",
@@ -124,8 +128,10 @@
124
128
  "node-edge-tts": "^1.2.10",
125
129
  "prompts": "^2.4.2",
126
130
  "resend": "^6.9.3",
131
+ "semver": "^7.7.4",
127
132
  "stripe": "^20.3.1",
128
133
  "toml": "^3.0.0",
129
- "tree-kill": "^1.2.2"
134
+ "tree-kill": "^1.2.2",
135
+ "xml2js": "^0.6.2"
130
136
  }
131
137
  }
package/bin/fraim-mcp.js DELETED
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * FRAIM MCP Server - Smart Entry Point
5
- * This file handles both production (dist/) and development (src/) environments.
6
- */
7
-
8
- const path = require('path');
9
- const fs = require('fs');
10
- const { spawn } = require('child_process');
11
-
12
- /**
13
- * Runs the MCP server using either the compiled JS or the source TS via tsx
14
- */
15
- function runMCPServer() {
16
- const distPath = path.join(__dirname, '..', 'dist', 'src', 'local-mcp-server', 'stdio-server.js');
17
- const srcPath = path.join(__dirname, '..', 'src', 'local-mcp-server', 'stdio-server.ts');
18
-
19
- // 1. Check if we have a compiled version (Production / CI)
20
- if (fs.existsSync(distPath)) {
21
- // Use spawn to run the compiled JS so it's treated as the main module
22
- const result = spawn('node', [distPath, ...process.argv.slice(2)], {
23
- stdio: 'inherit',
24
- shell: false,
25
- windowsHide: true
26
- });
27
-
28
- // Forward exit code
29
- result.on('exit', (code) => {
30
- process.exit(code || 0);
31
- });
32
-
33
- return;
34
- }
35
-
36
- // 2. Fallback to source version using tsx (Development)
37
- if (fs.existsSync(srcPath)) {
38
- // Use spawn to run tsx so it handles stdio correctly for MCP
39
- const result = spawn('npx', ['tsx', srcPath, ...process.argv.slice(2)], {
40
- stdio: 'inherit',
41
- shell: true,
42
- windowsHide: true
43
- });
44
-
45
- // Forward exit code
46
- result.on('exit', (code) => {
47
- process.exit(code || 0);
48
- });
49
-
50
- return;
51
- }
52
-
53
- console.error('āŒ FRAIM MCP Server Error: Could not find server entry point.');
54
- console.error('Expected one of:');
55
- console.error(` - ${distPath}`);
56
- console.error(` - ${srcPath}`);
57
- process.exit(1);
58
- }
59
-
60
- // If this file is run directly, run the MCP server
61
- if (require.main === module) {
62
- runMCPServer();
63
- }