fraim-framework 2.0.100 → 2.0.101
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/README.md +39 -17
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/init-project.js +6 -2
- package/dist/src/cli/commands/sync.js +12 -0
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/utils/fraim-gitignore.js +66 -15
- package/dist/src/core/config-loader.js +18 -0
- package/dist/src/core/utils/project-fraim-migration.js +12 -0
- package/dist/src/core/utils/workflow-parser.js +5 -3
- package/dist/src/local-mcp-server/stdio-server.js +298 -23
- package/dist/src/local-mcp-server/usage-collector.js +56 -38
- package/index.js +84 -84
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Current "vibe coding" frameworks are great at getting from idea to prototype. Th
|
|
|
8
8
|
|
|
9
9
|
AI agents are like brilliant but inexperienced developers. They need:
|
|
10
10
|
• Clear guardrails to prevent costly mistakes
|
|
11
|
-
• Structured
|
|
11
|
+
• Structured jobs, skills, and rules to avoid chaos
|
|
12
12
|
• Evidence-based validation (not "looks good" claims)
|
|
13
13
|
• Learning systems to improve over time
|
|
14
14
|
• Balance between determinism and creativity
|
|
@@ -23,7 +23,7 @@ FRAIM transforms you from a solo developer into an AI manager orchestrating mult
|
|
|
23
23
|
|
|
24
24
|
**Real Results:**
|
|
25
25
|
• Dramatic reduction in AI-generated code that needs rework
|
|
26
|
-
• Faster delivery through structured
|
|
26
|
+
• Faster delivery through structured jobs
|
|
27
27
|
• Higher test coverage through mandatory evidence collection
|
|
28
28
|
• Zero agent conflicts through phase-based coordination
|
|
29
29
|
|
|
@@ -44,11 +44,11 @@ R - Retrospectives: Continuous learning from experience
|
|
|
44
44
|
|
|
45
45
|
| **Human Development** | **AI Agent Development** | **FRAIM Solution** |
|
|
46
46
|
|----------------------|-------------------------|-------------------|
|
|
47
|
-
| **Code Reviews** | Random quality checks | Structured review
|
|
47
|
+
| **Code Reviews** | Random quality checks | Structured review jobs with evidence requirements |
|
|
48
48
|
| **Testing Standards** | "Looks good" claims | Mandatory test evidence with failure reproduction |
|
|
49
49
|
| **Team Coordination** | Agent conflicts and overlaps | Phase-based isolation with clear handoffs |
|
|
50
50
|
| **Learning Culture** | Repeated mistakes | Retrospective-driven improvement system |
|
|
51
|
-
| **Process Discipline** | Ad-hoc approaches | Proven
|
|
51
|
+
| **Process Discipline** | Ad-hoc approaches | Proven jobs, skills, and deterministic scripts from real projects |
|
|
52
52
|
| **Quality Gates** | Unreliable outcomes | Deterministic validation with rollback capabilities |
|
|
53
53
|
|
|
54
54
|
|
|
@@ -133,7 +133,7 @@ R - Retrospectives: Continuous learning from experience
|
|
|
133
133
|
|
|
134
134
|
#### 🔄 **Merge Requirements & Branch Safety**
|
|
135
135
|
**Problem**: Agents accidentally overwrite master branch or create merge conflicts
|
|
136
|
-
**Solution**: Mandatory rebase
|
|
136
|
+
**Solution**: Mandatory rebase discipline with conflict resolution patterns
|
|
137
137
|
```bash
|
|
138
138
|
# Before: Force pushes that destroy other work
|
|
139
139
|
# After: Rebase-on-master with force-with-lease for safety
|
|
@@ -159,7 +159,7 @@ R - Retrospectives: Continuous learning from experience
|
|
|
159
159
|
## 🚀 **Proven Benefits from Real Projects**
|
|
160
160
|
|
|
161
161
|
- **Dramatic reduction** in AI-generated code that needs rework through evidence-based validation
|
|
162
|
-
- **Faster delivery** through structured
|
|
162
|
+
- **Faster delivery** through structured jobs and clear handoffs
|
|
163
163
|
- **Higher test coverage** through mandatory testing guidelines and evidence collection
|
|
164
164
|
- **Zero agent conflicts** through phase-based isolation and coordination
|
|
165
165
|
- **Complete accountability** - agents fix their own mistakes with evidence
|
|
@@ -248,7 +248,7 @@ fraim setup --key=<your-fraim-key>
|
|
|
248
248
|
|
|
249
249
|
The setup command supports three modes:
|
|
250
250
|
|
|
251
|
-
**Conversational Mode**:
|
|
251
|
+
**Conversational Mode**: FRAIM job guidance only, no platform integration required
|
|
252
252
|
```bash
|
|
253
253
|
fraim setup --key=<your-fraim-key>
|
|
254
254
|
# Select "Conversational Mode" when prompted
|
|
@@ -299,19 +299,41 @@ fraim doctor --test-mcp # Test MCP server connections
|
|
|
299
299
|
fraim doctor # Diagnose configuration issues
|
|
300
300
|
|
|
301
301
|
# Sync and maintenance
|
|
302
|
-
fraim sync # Sync latest
|
|
302
|
+
fraim sync # Sync latest jobs, skills, rules, and templates
|
|
303
303
|
```
|
|
304
304
|
|
|
305
305
|
**💡 Pro Tip**: Use `fraim add-ide` when you install a new IDE after initial setup. It reuses your existing FRAIM and platform tokens, making it much faster than running full setup again.
|
|
306
306
|
|
|
307
|
+
### **Which Job Should I Run?**
|
|
308
|
+
|
|
309
|
+
FRAIM's primary execution unit is a **job**. Jobs define the phased path. Skills and rules support the job; they are not the thing you "run" first.
|
|
310
|
+
|
|
311
|
+
Use these defaults:
|
|
312
|
+
- `feature-specification` when the request is still fuzzy or needs clarified requirements, UX, or acceptance criteria.
|
|
313
|
+
- `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
|
|
314
|
+
- `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
|
|
315
|
+
- `test-execution` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
|
|
316
|
+
- `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
|
|
317
|
+
- `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
|
|
318
|
+
- `implementation-design-review` when you need to verify the code matches the approved technical design.
|
|
319
|
+
- `issue-retrospective` after the work is complete and you want durable learnings captured.
|
|
320
|
+
|
|
321
|
+
Typical path for a larger feature:
|
|
322
|
+
- `feature-specification` -> `technical-design` -> `feature-implementation` -> review job -> `issue-retrospective`
|
|
323
|
+
|
|
324
|
+
Typical path for a small bug fix:
|
|
325
|
+
- `feature-implementation` -> review job if needed -> `issue-retrospective`
|
|
326
|
+
|
|
327
|
+
Once FRAIM is connected in your IDE, ask your agent to `list FRAIM jobs` or name the specific job directly, for example: `Run the feature-implementation job for issue #123`.
|
|
328
|
+
|
|
307
329
|
### **🧩 Personalized Jobs, Skills, and Rules**
|
|
308
330
|
|
|
309
|
-
Project-specific customization now lives under
|
|
331
|
+
Project-specific customization now lives under `fraim/personalized-employee/`.
|
|
310
332
|
|
|
311
333
|
Recommended layout:
|
|
312
334
|
|
|
313
335
|
```text
|
|
314
|
-
|
|
336
|
+
fraim/
|
|
315
337
|
personalized-employee/
|
|
316
338
|
jobs/
|
|
317
339
|
skills/
|
|
@@ -327,12 +349,12 @@ fraim override --copy rules/engineering/architecture-standards.md
|
|
|
327
349
|
```
|
|
328
350
|
|
|
329
351
|
Guidance:
|
|
330
|
-
- Put phased job customizations in
|
|
331
|
-
- Put reusable local capability snippets in
|
|
332
|
-
- Put broad team conventions in
|
|
333
|
-
- Put local deliverable tweaks in
|
|
334
|
-
- Do not edit synced content under
|
|
335
|
-
- Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in
|
|
352
|
+
- Put phased job customizations in `fraim/personalized-employee/jobs/...`
|
|
353
|
+
- Put reusable local capability snippets in `fraim/personalized-employee/skills/...`
|
|
354
|
+
- Put broad team conventions in `fraim/personalized-employee/rules/...`
|
|
355
|
+
- Put local deliverable tweaks in `fraim/personalized-employee/templates/...`
|
|
356
|
+
- Do not edit synced content under `fraim/ai-employee/` or `fraim/ai-manager/`; `fraim sync` will overwrite it
|
|
357
|
+
- Legacy `.fraim/overrides/` is still read for compatibility, but new work should go in `fraim/personalized-employee/`
|
|
336
358
|
|
|
337
359
|
### **🔧 Jira Integration Setup**
|
|
338
360
|
|
|
@@ -382,7 +404,7 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json # For Clau
|
|
|
382
404
|
## 🌟 **Why FRAIM is the Future**
|
|
383
405
|
|
|
384
406
|
### **1. Proven in Production**
|
|
385
|
-
Every rule,
|
|
407
|
+
Every rule, job, skill, and pattern has been tested in real projects. This isn't theoretical—it is battle-tested.
|
|
386
408
|
|
|
387
409
|
### **2. Enterprise Discipline**
|
|
388
410
|
The same rigor you'd apply to managing human developers, applied to AI agents.
|
package/bin/fraim.js
CHANGED
|
@@ -266,8 +266,12 @@ const runInitProject = async () => {
|
|
|
266
266
|
(0, project_bootstrap_1.recordPathStatus)(result, (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(dir), false);
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
270
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
271
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
272
|
+
}
|
|
273
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
274
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
271
275
|
}
|
|
272
276
|
const detection = (0, platform_detection_1.detectPlatformFromGit)();
|
|
273
277
|
if (detection.provider === 'github') {
|
|
@@ -46,6 +46,7 @@ const version_utils_1 = require("../utils/version-utils");
|
|
|
46
46
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
47
47
|
const git_utils_1 = require("../../core/utils/git-utils");
|
|
48
48
|
const agent_adapters_1 = require("../utils/agent-adapters");
|
|
49
|
+
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
49
50
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
50
51
|
/**
|
|
51
52
|
* Load API key from user-level config (~/.fraim/config.json)
|
|
@@ -86,6 +87,15 @@ const runSync = async (options) => {
|
|
|
86
87
|
const projectRoot = process.cwd();
|
|
87
88
|
const config = (0, config_loader_1.loadFraimConfig)();
|
|
88
89
|
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot);
|
|
90
|
+
const refreshLocalIgnoreConfig = () => {
|
|
91
|
+
const ignoreUpdate = (0, fraim_gitignore_1.ensureFraimSyncedContentLocallyExcluded)(projectRoot);
|
|
92
|
+
if (ignoreUpdate.gitInfoExcludeUpdated) {
|
|
93
|
+
console.log(chalk_1.default.green('Updated .git/info/exclude FRAIM managed block'));
|
|
94
|
+
}
|
|
95
|
+
if (ignoreUpdate.gitignoreUpdated) {
|
|
96
|
+
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
89
99
|
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
90
100
|
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
91
101
|
if (isGlobal && !options.skipUpdates) {
|
|
@@ -106,6 +116,7 @@ const runSync = async (options) => {
|
|
|
106
116
|
if (result.success) {
|
|
107
117
|
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
|
|
108
118
|
updateVersionInConfig(fraimDir);
|
|
119
|
+
refreshLocalIgnoreConfig();
|
|
109
120
|
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
|
|
110
121
|
if (adapterUpdates.length > 0) {
|
|
111
122
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
@@ -148,6 +159,7 @@ const runSync = async (options) => {
|
|
|
148
159
|
}
|
|
149
160
|
console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
|
|
150
161
|
updateVersionInConfig(fraimDir);
|
|
162
|
+
refreshLocalIgnoreConfig();
|
|
151
163
|
const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
|
|
152
164
|
if (adapterUpdates.length > 0) {
|
|
153
165
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
@@ -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;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.ensureFraimSyncedContentLocallyExcluded = exports.removeFraimSyncedContentGitignoreBlock = exports.FRAIM_SYNC_GITIGNORE_ENTRIES = exports.FRAIM_SYNC_GITIGNORE_END = exports.FRAIM_SYNC_GITIGNORE_START = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
|
|
@@ -14,27 +14,78 @@ exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
|
|
|
14
14
|
'fraim/docs/',
|
|
15
15
|
];
|
|
16
16
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const managedBlock = `${exports.FRAIM_SYNC_GITIGNORE_START}\n${exports.FRAIM_SYNC_GITIGNORE_ENTRIES.join('\n')}\n${exports.FRAIM_SYNC_GITIGNORE_END}`;
|
|
25
|
-
const blockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
26
|
-
const withoutManagedBlock = normalized.replace(blockPattern, '\n').trimEnd();
|
|
17
|
+
const managedBlockPattern = new RegExp(`\\n?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
18
|
+
function buildManagedBlock() {
|
|
19
|
+
return `${exports.FRAIM_SYNC_GITIGNORE_START}\n${exports.FRAIM_SYNC_GITIGNORE_ENTRIES.join('\n')}\n${exports.FRAIM_SYNC_GITIGNORE_END}`;
|
|
20
|
+
}
|
|
21
|
+
function normalizeWithoutManagedBlock(raw) {
|
|
22
|
+
const normalized = raw.replace(/\r\n/g, '\n');
|
|
23
|
+
const withoutManagedBlock = normalized.replace(managedBlockPattern, '\n').trimEnd();
|
|
27
24
|
const cleaned = withoutManagedBlock
|
|
28
25
|
.replace(/\n{3,}/g, '\n\n')
|
|
29
26
|
.replace(/^\n+/, '')
|
|
30
27
|
.trimEnd();
|
|
28
|
+
return cleaned;
|
|
29
|
+
}
|
|
30
|
+
function resolveGitInfoExcludePath(projectRoot) {
|
|
31
|
+
const gitPath = path_1.default.join(projectRoot, '.git');
|
|
32
|
+
if (!fs_1.default.existsSync(gitPath)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (fs_1.default.statSync(gitPath).isDirectory()) {
|
|
36
|
+
return path_1.default.join(gitPath, 'info', 'exclude');
|
|
37
|
+
}
|
|
38
|
+
const gitPointer = fs_1.default.readFileSync(gitPath, 'utf8');
|
|
39
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)\s*$/im);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const gitDir = path_1.default.isAbsolute(match[1])
|
|
44
|
+
? match[1]
|
|
45
|
+
: path_1.default.resolve(projectRoot, match[1]);
|
|
46
|
+
return path_1.default.join(gitDir, 'info', 'exclude');
|
|
47
|
+
}
|
|
48
|
+
const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
|
|
49
|
+
const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
|
|
50
|
+
if (!fs_1.default.existsSync(gitignorePath)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const existingRaw = fs_1.default.readFileSync(gitignorePath, 'utf8');
|
|
54
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
55
|
+
const cleaned = normalizeWithoutManagedBlock(existingRaw);
|
|
56
|
+
if (cleaned === normalized.trimEnd()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
60
|
+
const next = cleaned.length > 0 ? `${cleaned}\n` : '';
|
|
61
|
+
fs_1.default.writeFileSync(gitignorePath, next.replace(/\n/g, newline), 'utf8');
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
exports.removeFraimSyncedContentGitignoreBlock = removeFraimSyncedContentGitignoreBlock;
|
|
65
|
+
const ensureFraimSyncedContentLocallyExcluded = (projectRoot) => {
|
|
66
|
+
const gitignoreUpdated = (0, exports.removeFraimSyncedContentGitignoreBlock)(projectRoot);
|
|
67
|
+
const excludePath = resolveGitInfoExcludePath(projectRoot);
|
|
68
|
+
if (!excludePath) {
|
|
69
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: false };
|
|
70
|
+
}
|
|
71
|
+
const infoDir = path_1.default.dirname(excludePath);
|
|
72
|
+
if (!fs_1.default.existsSync(infoDir)) {
|
|
73
|
+
fs_1.default.mkdirSync(infoDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
const existingRaw = fs_1.default.existsSync(excludePath)
|
|
76
|
+
? fs_1.default.readFileSync(excludePath, 'utf8')
|
|
77
|
+
: '';
|
|
78
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
79
|
+
const cleaned = normalizeWithoutManagedBlock(existingRaw);
|
|
80
|
+
const managedBlock = buildManagedBlock();
|
|
31
81
|
const next = cleaned.length > 0
|
|
32
82
|
? `${cleaned}\n\n${managedBlock}\n`
|
|
33
83
|
: `${managedBlock}\n`;
|
|
34
84
|
if (next !== normalized) {
|
|
35
|
-
|
|
36
|
-
|
|
85
|
+
const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
86
|
+
fs_1.default.writeFileSync(excludePath, next.replace(/\n/g, newline), 'utf8');
|
|
87
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: true };
|
|
37
88
|
}
|
|
38
|
-
return false;
|
|
89
|
+
return { gitignoreUpdated, gitInfoExcludeUpdated: false };
|
|
39
90
|
};
|
|
40
|
-
exports.
|
|
91
|
+
exports.ensureFraimSyncedContentLocallyExcluded = ensureFraimSyncedContentLocallyExcluded;
|
|
@@ -11,6 +11,20 @@ exports.getRepositoryInfo = getRepositoryInfo;
|
|
|
11
11
|
const fs_1 = require("fs");
|
|
12
12
|
const types_1 = require("./types");
|
|
13
13
|
const project_fraim_paths_1 = require("./utils/project-fraim-paths");
|
|
14
|
+
function normalizeCustomerCommunication(config) {
|
|
15
|
+
const current = config?.['customer-communication'];
|
|
16
|
+
if (!current || typeof current !== 'object')
|
|
17
|
+
return undefined;
|
|
18
|
+
return {
|
|
19
|
+
productName: current.productName,
|
|
20
|
+
productUrl: current.productUrl,
|
|
21
|
+
senderDisplayName: current.senderDisplayName,
|
|
22
|
+
senderEmail: current.senderEmail,
|
|
23
|
+
senderReplyTo: current.senderReplyTo,
|
|
24
|
+
newsletterAudienceProvider: current.newsletterAudienceProvider,
|
|
25
|
+
deliveryProvider: current.deliveryProvider
|
|
26
|
+
};
|
|
27
|
+
}
|
|
14
28
|
function normalizeFraimConfig(config) {
|
|
15
29
|
// Handle backward compatibility and migration
|
|
16
30
|
const mergedConfig = {
|
|
@@ -52,6 +66,10 @@ function normalizeFraimConfig(config) {
|
|
|
52
66
|
if (config.competitors && typeof config.competitors === 'object') {
|
|
53
67
|
mergedConfig.competitors = config.competitors;
|
|
54
68
|
}
|
|
69
|
+
const customerCommunication = normalizeCustomerCommunication(config);
|
|
70
|
+
if (customerCommunication) {
|
|
71
|
+
mergedConfig.customerCommunication = customerCommunication;
|
|
72
|
+
}
|
|
55
73
|
return mergedConfig;
|
|
56
74
|
}
|
|
57
75
|
/**
|
|
@@ -90,6 +90,11 @@ function migrateLegacyProjectFraimDir(projectRoot = process.cwd()) {
|
|
|
90
90
|
for (const entry of MIGRATABLE_LEGACY_ENTRIES) {
|
|
91
91
|
moveOrMergePath(path_1.default.join(legacyDirPath, entry), path_1.default.join(workspaceDirPath, entry), entry, result);
|
|
92
92
|
}
|
|
93
|
+
// Handle learned-skills -> personalized-employee/skills mapping
|
|
94
|
+
const legacyLearnedSkillsPath = path_1.default.join(legacyDirPath, 'learned-skills');
|
|
95
|
+
if (fs_1.default.existsSync(legacyLearnedSkillsPath)) {
|
|
96
|
+
moveOrMergePath(legacyLearnedSkillsPath, path_1.default.join(workspaceDirPath, 'personalized-employee', 'skills'), 'learned-skills', result);
|
|
97
|
+
}
|
|
93
98
|
for (const entry of PRUNABLE_LEGACY_SYNCED_ENTRIES) {
|
|
94
99
|
const legacyEntryPath = path_1.default.join(legacyDirPath, entry);
|
|
95
100
|
if (!fs_1.default.existsSync(legacyEntryPath)) {
|
|
@@ -98,6 +103,13 @@ function migrateLegacyProjectFraimDir(projectRoot = process.cwd()) {
|
|
|
98
103
|
fs_1.default.rmSync(legacyEntryPath, { recursive: true, force: true });
|
|
99
104
|
result.prunedLegacyPaths.push(entry);
|
|
100
105
|
}
|
|
106
|
+
// Final check for entries that are neither migratable nor prunable
|
|
107
|
+
const remainingEntries = fs_1.default.readdirSync(legacyDirPath);
|
|
108
|
+
const coreEntries = [...MIGRATABLE_LEGACY_ENTRIES, ...PRUNABLE_LEGACY_SYNCED_ENTRIES, 'learned-skills'];
|
|
109
|
+
const unknownEntries = remainingEntries.filter(e => !coreEntries.includes(e));
|
|
110
|
+
if (unknownEntries.length > 0) {
|
|
111
|
+
result.conflictPaths.push(...unknownEntries);
|
|
112
|
+
}
|
|
101
113
|
result.legacyDirReadyForDeletion = result.conflictPaths.length === 0 && isDirectoryEmpty(legacyDirPath);
|
|
102
114
|
return result;
|
|
103
115
|
}
|
|
@@ -5,13 +5,15 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
7
|
static extractMetadataBlock(content) {
|
|
8
|
-
|
|
8
|
+
// Allow leading comments and whitespace before frontmatter
|
|
9
|
+
const frontmatterMatch = content.match(/^[\s\S]*?---\r?\n([\s\S]+?)\r?\n---/);
|
|
9
10
|
if (frontmatterMatch) {
|
|
10
11
|
try {
|
|
12
|
+
const startIndex = frontmatterMatch.index || 0;
|
|
11
13
|
return {
|
|
12
14
|
state: 'valid',
|
|
13
15
|
metadata: JSON.parse(frontmatterMatch[1]),
|
|
14
|
-
bodyStartIndex: frontmatterMatch[0].length
|
|
16
|
+
bodyStartIndex: startIndex + frontmatterMatch[0].length
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
@@ -107,7 +109,7 @@ class WorkflowParser {
|
|
|
107
109
|
overview = contentAfterMetadata;
|
|
108
110
|
}
|
|
109
111
|
const phases = new Map();
|
|
110
|
-
const phaseSections = restOfContent.split(/^##\s+Phase:\s
|
|
112
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s*/im);
|
|
111
113
|
if (!metadata.phases) {
|
|
112
114
|
metadata.phases = {};
|
|
113
115
|
}
|
|
@@ -81,7 +81,12 @@ class FraimTemplateEngine {
|
|
|
81
81
|
return this.userEmail;
|
|
82
82
|
}
|
|
83
83
|
substituteTemplates(content) {
|
|
84
|
+
return this.substituteTemplatesWithNotices(content).content;
|
|
85
|
+
}
|
|
86
|
+
substituteTemplatesWithNotices(content) {
|
|
84
87
|
let result = content;
|
|
88
|
+
const notices = [];
|
|
89
|
+
const blockingRequirements = [];
|
|
85
90
|
// Substitute {{proxy.user.email}} with the email captured from fraim_connect
|
|
86
91
|
if (this.userEmail) {
|
|
87
92
|
result = result.replace(/\{\{proxy\.user\.email\}\}/g, this.userEmail);
|
|
@@ -103,22 +108,17 @@ class FraimTemplateEngine {
|
|
|
103
108
|
return match;
|
|
104
109
|
});
|
|
105
110
|
// First, substitute config variables with fallback support.
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return fallback !== undefined ? fallback : match;
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
return fallback !== undefined ? fallback : match;
|
|
121
|
-
}
|
|
111
|
+
// Supported forms:
|
|
112
|
+
// {{proxy.config.path}}
|
|
113
|
+
// {{proxy.config.path | "fallback"}}
|
|
114
|
+
// {{proxy.config.path | INFORM | "fallback behavior"}}
|
|
115
|
+
// {{proxy.config.path | NO_OP | "fallback behavior"}}
|
|
116
|
+
// {{proxy.config.path | REQUIRE | "fallback behavior"}}
|
|
117
|
+
// Note: \s* tolerates optional whitespace inside {{...}} (e.g. from remote server formatting).
|
|
118
|
+
// This must match the same set of templates that rewriteProxyTokensInText can rewrite,
|
|
119
|
+
// so that config templates are always resolved here rather than falling through to the fallback rewriter.
|
|
120
|
+
result = result.replace(/\{\{\s*proxy\.config\.([^}]+?)\s*\}\}/g, (match, expression) => {
|
|
121
|
+
return this.resolveConfigTemplate(match, expression, notices, blockingRequirements);
|
|
122
122
|
});
|
|
123
123
|
// Second, substitute {{proxy.delivery.*}} templates
|
|
124
124
|
const deliveryValues = this.loadDeliveryTemplates();
|
|
@@ -130,7 +130,130 @@ class FraimTemplateEngine {
|
|
|
130
130
|
}
|
|
131
131
|
// Third, substitute platform-specific action templates
|
|
132
132
|
result = this.substitutePlatformActions(result);
|
|
133
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
content: result,
|
|
135
|
+
notices,
|
|
136
|
+
blockingRequirements
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
resolveConfigTemplate(originalMatch, expression, notices, blockingRequirements) {
|
|
140
|
+
const segments = this.splitTemplateSegments(expression);
|
|
141
|
+
if (segments.length === 0)
|
|
142
|
+
return originalMatch;
|
|
143
|
+
const path = segments[0].trim();
|
|
144
|
+
const remainder = segments.slice(1).map(segment => segment.trim()).filter(Boolean);
|
|
145
|
+
let action = null;
|
|
146
|
+
let fallback;
|
|
147
|
+
if (remainder.length === 1) {
|
|
148
|
+
fallback = this.parseQuotedTemplateValue(remainder[0]);
|
|
149
|
+
if (fallback === undefined)
|
|
150
|
+
return originalMatch;
|
|
151
|
+
}
|
|
152
|
+
else if (remainder.length === 2) {
|
|
153
|
+
const candidateAction = remainder[0].toUpperCase();
|
|
154
|
+
if (!FraimTemplateEngine.CONFIG_TEMPLATE_ACTIONS.has(candidateAction)) {
|
|
155
|
+
return originalMatch;
|
|
156
|
+
}
|
|
157
|
+
fallback = this.parseQuotedTemplateValue(remainder[1]);
|
|
158
|
+
if (fallback === undefined)
|
|
159
|
+
return originalMatch;
|
|
160
|
+
action = candidateAction;
|
|
161
|
+
}
|
|
162
|
+
else if (remainder.length > 0) {
|
|
163
|
+
return originalMatch;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (this.config) {
|
|
167
|
+
const value = (0, object_utils_1.getNestedValue)(this.config, path);
|
|
168
|
+
if (value !== undefined) {
|
|
169
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (fallback === undefined) {
|
|
173
|
+
return originalMatch;
|
|
174
|
+
}
|
|
175
|
+
if (action === null || action === 'NO_OP') {
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
178
|
+
if (action === 'REQUIRE') {
|
|
179
|
+
blockingRequirements.push(this.buildMissingConfigNotice(path, action));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
notices.push(this.buildMissingConfigNotice(path, action));
|
|
183
|
+
}
|
|
184
|
+
return fallback;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (fallback === undefined) {
|
|
188
|
+
return originalMatch;
|
|
189
|
+
}
|
|
190
|
+
if (action === null || action === 'NO_OP') {
|
|
191
|
+
return fallback;
|
|
192
|
+
}
|
|
193
|
+
if (action === 'REQUIRE') {
|
|
194
|
+
blockingRequirements.push(this.buildMissingConfigNotice(path, action));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
notices.push(this.buildMissingConfigNotice(path, action));
|
|
198
|
+
}
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
splitTemplateSegments(expression) {
|
|
203
|
+
const segments = [];
|
|
204
|
+
let current = '';
|
|
205
|
+
let inQuotes = false;
|
|
206
|
+
for (let i = 0; i < expression.length; i += 1) {
|
|
207
|
+
const char = expression[i];
|
|
208
|
+
const prev = i > 0 ? expression[i - 1] : '';
|
|
209
|
+
if (char === '"' && prev !== '\\') {
|
|
210
|
+
inQuotes = !inQuotes;
|
|
211
|
+
current += char;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (char === '|' && !inQuotes) {
|
|
215
|
+
segments.push(current);
|
|
216
|
+
current = '';
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
current += char;
|
|
220
|
+
}
|
|
221
|
+
segments.push(current);
|
|
222
|
+
return segments;
|
|
223
|
+
}
|
|
224
|
+
parseQuotedTemplateValue(segment) {
|
|
225
|
+
const trimmed = segment.trim();
|
|
226
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(trimmed);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
buildMissingConfigNotice(path, action) {
|
|
237
|
+
const metadata = FraimTemplateEngine.CONFIG_CONTEXT_METADATA[path];
|
|
238
|
+
const label = metadata?.label || `config value \`${path}\``;
|
|
239
|
+
const onboardingHint = metadata?.onboardingHint || `provide \`${path}\` in \`fraim/config.json\``;
|
|
240
|
+
if (action === 'REQUIRE') {
|
|
241
|
+
return {
|
|
242
|
+
action,
|
|
243
|
+
path,
|
|
244
|
+
message: `${this.capitalizeFirst(label)} is required for this task but is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint} before continuing.`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
action,
|
|
249
|
+
path,
|
|
250
|
+
message: `${this.capitalizeFirst(label)} is not configured. Invoke the manager \`project-onboarding\` job and ${onboardingHint}.`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
capitalizeFirst(value) {
|
|
254
|
+
if (!value)
|
|
255
|
+
return value;
|
|
256
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
134
257
|
}
|
|
135
258
|
loadDeliveryTemplates() {
|
|
136
259
|
if (this.deliveryTemplatesCache)
|
|
@@ -235,6 +358,25 @@ class FraimTemplateEngine {
|
|
|
235
358
|
}
|
|
236
359
|
}
|
|
237
360
|
exports.FraimTemplateEngine = FraimTemplateEngine;
|
|
361
|
+
FraimTemplateEngine.CONFIG_TEMPLATE_ACTIONS = new Set(['INFORM', 'NO_OP', 'REQUIRE']);
|
|
362
|
+
FraimTemplateEngine.CONFIG_CONTEXT_METADATA = {
|
|
363
|
+
'customizations.architectureDoc': {
|
|
364
|
+
label: 'project-specific architecture document',
|
|
365
|
+
onboardingHint: 'provide the architecture document path in `fraim/config.json`'
|
|
366
|
+
},
|
|
367
|
+
'customizations.designSystem.path': {
|
|
368
|
+
label: 'project-specific design system',
|
|
369
|
+
onboardingHint: 'provide the design system path in `fraim/config.json`'
|
|
370
|
+
},
|
|
371
|
+
compliance: {
|
|
372
|
+
label: 'project compliance configuration',
|
|
373
|
+
onboardingHint: 'record the project compliance requirements in `fraim/config.json`'
|
|
374
|
+
},
|
|
375
|
+
'compliance.regulations': {
|
|
376
|
+
label: 'project compliance regulations',
|
|
377
|
+
onboardingHint: 'record the applicable compliance regulations in `fraim/config.json`'
|
|
378
|
+
}
|
|
379
|
+
};
|
|
238
380
|
FraimTemplateEngine.PROXY_ACTION_PREFIX = 'proxy.action.';
|
|
239
381
|
FraimTemplateEngine.ISSUE_ACTIONS = new Set([
|
|
240
382
|
'get_issue',
|
|
@@ -798,6 +940,11 @@ class FraimLocalMCPServer {
|
|
|
798
940
|
await this.ensureProviderTemplatesAvailable(processedResponse, requestSessionId);
|
|
799
941
|
processedResponse = this.processResponse(processedResponse);
|
|
800
942
|
}
|
|
943
|
+
const blockingResponse = this.failForMissingRequiredConfig(processedResponse);
|
|
944
|
+
if (blockingResponse) {
|
|
945
|
+
return blockingResponse;
|
|
946
|
+
}
|
|
947
|
+
processedResponse = this.prependMissingConfigNotices(processedResponse);
|
|
801
948
|
return this.applyAgentFallbackForUnresolvedProxy(processedResponse);
|
|
802
949
|
}
|
|
803
950
|
async resolveIncludesInResponse(response, requestSessionId, requestId) {
|
|
@@ -856,12 +1003,11 @@ class FraimLocalMCPServer {
|
|
|
856
1003
|
// personalized-employee/ takes priority; when remote fails, resolver falls back to synced/cached content
|
|
857
1004
|
const resolved = await resolver.resolveFile(registryPath).catch(() => null);
|
|
858
1005
|
if (resolved?.content) {
|
|
859
|
-
const substitutedContent = this.substituteTemplates(resolved.content);
|
|
860
1006
|
const newResponse = {
|
|
861
1007
|
...response,
|
|
862
1008
|
result: {
|
|
863
1009
|
...response.result,
|
|
864
|
-
content: [{ type: 'text', text:
|
|
1010
|
+
content: [{ type: 'text', text: resolved.content }]
|
|
865
1011
|
}
|
|
866
1012
|
};
|
|
867
1013
|
delete newResponse.error;
|
|
@@ -925,12 +1071,24 @@ class FraimLocalMCPServer {
|
|
|
925
1071
|
const tokens = new Set();
|
|
926
1072
|
const rewritten = text.replace(/\{\{\s*proxy\.([^}]+?)\s*\}\}/g, (_match, proxyPath) => {
|
|
927
1073
|
const normalized = proxyPath.trim();
|
|
1074
|
+
// proxy.config.* must never reach here — substituteTemplatesWithNotices resolves all of
|
|
1075
|
+
// them (with config values or fallback text). If one slips through it is a bug; strip it
|
|
1076
|
+
// and log rather than rewriting to {{agent.config.*}} which confuses the agent (issue #210).
|
|
1077
|
+
if (normalized.startsWith('config.')) {
|
|
1078
|
+
this.logError(`[${FraimLocalMCPServer.FALLBACK_ALERT_MARKER}] BUG: unresolved proxy.${normalized} survived processResponse — stripping`);
|
|
1079
|
+
return '';
|
|
1080
|
+
}
|
|
928
1081
|
tokens.add(`proxy.${normalized}`);
|
|
929
1082
|
return `{{agent.${normalized}}}`;
|
|
930
1083
|
});
|
|
931
|
-
|
|
1084
|
+
// If nothing changed at all, return early (no allocation, no notice).
|
|
1085
|
+
if (rewritten === text) {
|
|
932
1086
|
return { text, tokens: [] };
|
|
933
1087
|
}
|
|
1088
|
+
// Only prepend the agent-resolution notice when there are actual agent tokens to resolve.
|
|
1089
|
+
if (tokens.size === 0) {
|
|
1090
|
+
return { text: rewritten, tokens: [] };
|
|
1091
|
+
}
|
|
934
1092
|
const hasResolutionNotice = rewritten.includes('## Agent Resolution Needed');
|
|
935
1093
|
const finalText = hasResolutionNotice
|
|
936
1094
|
? rewritten
|
|
@@ -970,6 +1128,84 @@ class FraimLocalMCPServer {
|
|
|
970
1128
|
substituteTemplates(content) {
|
|
971
1129
|
return this.ensureEngine().substituteTemplates(content);
|
|
972
1130
|
}
|
|
1131
|
+
prependMissingConfigNotices(response) {
|
|
1132
|
+
if (!response.result?.content || !Array.isArray(response.result.content)) {
|
|
1133
|
+
return response;
|
|
1134
|
+
}
|
|
1135
|
+
const contentBlocks = response.result.content;
|
|
1136
|
+
const firstTextIndex = contentBlocks.findIndex((block) => block?.type === 'text' && typeof block.text === 'string');
|
|
1137
|
+
if (firstTextIndex === -1) {
|
|
1138
|
+
return response;
|
|
1139
|
+
}
|
|
1140
|
+
const existingText = contentBlocks[firstTextIndex].text;
|
|
1141
|
+
const notices = response.result.__proxyMissingConfigNotices;
|
|
1142
|
+
if (!notices || notices.length === 0) {
|
|
1143
|
+
return response;
|
|
1144
|
+
}
|
|
1145
|
+
const uniqueNotices = [];
|
|
1146
|
+
const seen = new Set();
|
|
1147
|
+
for (const notice of notices) {
|
|
1148
|
+
const key = `${notice.action}:${notice.path}:${notice.message}`;
|
|
1149
|
+
if (seen.has(key))
|
|
1150
|
+
continue;
|
|
1151
|
+
seen.add(key);
|
|
1152
|
+
uniqueNotices.push(notice);
|
|
1153
|
+
}
|
|
1154
|
+
if (uniqueNotices.length === 0) {
|
|
1155
|
+
return response;
|
|
1156
|
+
}
|
|
1157
|
+
const noticeLines = ['## Missing Project Context'];
|
|
1158
|
+
for (const notice of uniqueNotices) {
|
|
1159
|
+
noticeLines.push(`- ${notice.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
const prefix = `${noticeLines.join('\n')}\n\n---\n\n`;
|
|
1162
|
+
const transformedContent = contentBlocks.map((block, index) => {
|
|
1163
|
+
if (index !== firstTextIndex)
|
|
1164
|
+
return block;
|
|
1165
|
+
return {
|
|
1166
|
+
...block,
|
|
1167
|
+
text: prefix + existingText
|
|
1168
|
+
};
|
|
1169
|
+
});
|
|
1170
|
+
const { __proxyMissingConfigNotices, ...restResult } = response.result;
|
|
1171
|
+
return {
|
|
1172
|
+
...response,
|
|
1173
|
+
result: {
|
|
1174
|
+
...restResult,
|
|
1175
|
+
content: transformedContent
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
failForMissingRequiredConfig(response) {
|
|
1180
|
+
const requirements = response.result?.__proxyMissingConfigRequirements;
|
|
1181
|
+
if (!requirements || requirements.length === 0) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
const uniqueRequirements = [];
|
|
1185
|
+
const seen = new Set();
|
|
1186
|
+
for (const requirement of requirements) {
|
|
1187
|
+
const key = `${requirement.action}:${requirement.path}:${requirement.message}`;
|
|
1188
|
+
if (seen.has(key))
|
|
1189
|
+
continue;
|
|
1190
|
+
seen.add(key);
|
|
1191
|
+
uniqueRequirements.push(requirement);
|
|
1192
|
+
}
|
|
1193
|
+
const messageLines = ['Required project context is missing:'];
|
|
1194
|
+
for (const requirement of uniqueRequirements) {
|
|
1195
|
+
messageLines.push(`- ${requirement.message}`);
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
jsonrpc: response.jsonrpc,
|
|
1199
|
+
id: response.id ?? null,
|
|
1200
|
+
error: {
|
|
1201
|
+
code: -32003,
|
|
1202
|
+
message: messageLines.join('\n'),
|
|
1203
|
+
data: {
|
|
1204
|
+
missingConfigPaths: uniqueRequirements.map((requirement) => requirement.path)
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
973
1209
|
/**
|
|
974
1210
|
* Initialize the LocalRegistryResolver for override resolution
|
|
975
1211
|
*/
|
|
@@ -1057,10 +1293,15 @@ class FraimLocalMCPServer {
|
|
|
1057
1293
|
processResponse(response) {
|
|
1058
1294
|
if (!response.result)
|
|
1059
1295
|
return response;
|
|
1296
|
+
const notices = [];
|
|
1297
|
+
const blockingRequirements = [];
|
|
1060
1298
|
// Recursively substitute templates in all string values
|
|
1061
1299
|
const processValue = (value) => {
|
|
1062
1300
|
if (typeof value === 'string') {
|
|
1063
|
-
|
|
1301
|
+
const substitution = this.ensureEngine().substituteTemplatesWithNotices(value);
|
|
1302
|
+
notices.push(...substitution.notices);
|
|
1303
|
+
blockingRequirements.push(...substitution.blockingRequirements);
|
|
1304
|
+
return substitution.content;
|
|
1064
1305
|
}
|
|
1065
1306
|
else if (Array.isArray(value)) {
|
|
1066
1307
|
return value.map(processValue);
|
|
@@ -1074,14 +1315,25 @@ class FraimLocalMCPServer {
|
|
|
1074
1315
|
}
|
|
1075
1316
|
return value;
|
|
1076
1317
|
};
|
|
1318
|
+
const processedResult = processValue(response.result);
|
|
1319
|
+
if (notices.length > 0 && processedResult && typeof processedResult === 'object') {
|
|
1320
|
+
processedResult.__proxyMissingConfigNotices = notices;
|
|
1321
|
+
}
|
|
1322
|
+
if (blockingRequirements.length > 0 && processedResult && typeof processedResult === 'object') {
|
|
1323
|
+
processedResult.__proxyMissingConfigRequirements = blockingRequirements;
|
|
1324
|
+
}
|
|
1077
1325
|
return {
|
|
1078
1326
|
...response,
|
|
1079
|
-
result:
|
|
1327
|
+
result: processedResult
|
|
1080
1328
|
};
|
|
1081
1329
|
}
|
|
1082
1330
|
applyAgentFallbackForUnresolvedProxy(response) {
|
|
1083
1331
|
if (!response.result)
|
|
1084
1332
|
return response;
|
|
1333
|
+
// Rewrite any unresolved {{proxy.*}} placeholders to {{agent.*}} so the agent can
|
|
1334
|
+
// handle them. proxy.config.* is excluded inside rewriteProxyTokensInText — those
|
|
1335
|
+
// must have been fully resolved by substituteTemplatesWithNotices; any that survive
|
|
1336
|
+
// are bugs and get stripped with a log warning rather than rewritten.
|
|
1085
1337
|
const rewritten = this.rewriteUnresolvedProxyPlaceholders(response.result);
|
|
1086
1338
|
if (rewritten.tokens.length > 0) {
|
|
1087
1339
|
this.logError(`[${FraimLocalMCPServer.FALLBACK_ALERT_MARKER}] Rewrote unresolved proxy placeholders to agent placeholders: ${rewritten.tokens.join(', ')}`);
|
|
@@ -1249,7 +1501,30 @@ class FraimLocalMCPServer {
|
|
|
1249
1501
|
// Resolve templates in the outgoing request so the remote server
|
|
1250
1502
|
// only ever sees finalized values.
|
|
1251
1503
|
const stringifiedRequest = JSON.stringify(request);
|
|
1252
|
-
const
|
|
1504
|
+
const requestSubstitution = this.ensureEngine().substituteTemplatesWithNotices(stringifiedRequest);
|
|
1505
|
+
if (requestSubstitution.blockingRequirements.length > 0) {
|
|
1506
|
+
const uniqueRequirements = [];
|
|
1507
|
+
const seen = new Set();
|
|
1508
|
+
for (const requirement of requestSubstitution.blockingRequirements) {
|
|
1509
|
+
const key = `${requirement.action}:${requirement.path}:${requirement.message}`;
|
|
1510
|
+
if (seen.has(key))
|
|
1511
|
+
continue;
|
|
1512
|
+
seen.add(key);
|
|
1513
|
+
uniqueRequirements.push(requirement);
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
jsonrpc: '2.0',
|
|
1517
|
+
id: request.id,
|
|
1518
|
+
error: {
|
|
1519
|
+
code: -32003,
|
|
1520
|
+
message: ['Required project context is missing:', ...uniqueRequirements.map((requirement) => `- ${requirement.message}`)].join('\n'),
|
|
1521
|
+
data: {
|
|
1522
|
+
missingConfigPaths: uniqueRequirements.map((requirement) => requirement.path)
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
const resolvedRequestStr = requestSubstitution.content;
|
|
1253
1528
|
const finalRequest = JSON.parse(resolvedRequestStr);
|
|
1254
1529
|
const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, finalRequest, {
|
|
1255
1530
|
headers,
|
|
@@ -10,8 +10,8 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
10
10
|
// The server will override this with the correct ID from the authenticated API key.
|
|
11
11
|
const PLACEHOLDER_API_KEY_ID = new mongodb_1.ObjectId('000000000000000000000000');
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* UsageCollector is responsible for collecting usage events from MCP tools
|
|
14
|
+
* and formatting them for the analytics system.
|
|
15
15
|
*/
|
|
16
16
|
class UsageCollector {
|
|
17
17
|
constructor() {
|
|
@@ -53,19 +53,24 @@ class UsageCollector {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
// Extract useful args for analytics
|
|
56
|
-
const analyticsArgs = this.extractAnalyticsArgs(toolName, args);
|
|
56
|
+
const analyticsArgs = this.extractAnalyticsArgs(toolName, args) || {};
|
|
57
|
+
// Ensure category from path parsing is included in args
|
|
58
|
+
if (parsed.category && !analyticsArgs.category) {
|
|
59
|
+
analyticsArgs.category = parsed.category;
|
|
60
|
+
}
|
|
57
61
|
const event = {
|
|
58
62
|
type: parsed.type,
|
|
59
63
|
name: parsed.name,
|
|
60
64
|
// Use set apiKeyId if available, otherwise a placeholder.
|
|
61
|
-
// The server will override this with the correct
|
|
65
|
+
// The server will override this with the correct ID from the authenticated API key.
|
|
62
66
|
apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
|
|
63
67
|
sessionId,
|
|
64
68
|
success,
|
|
65
|
-
|
|
69
|
+
category: parsed.category,
|
|
70
|
+
args: Object.keys(analyticsArgs).length > 0 ? analyticsArgs : undefined
|
|
66
71
|
};
|
|
67
72
|
this.events.push(event);
|
|
68
|
-
const successMsg = `[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (session: ${sessionId}, queue: ${this.events.length})`;
|
|
73
|
+
const successMsg = `[UsageCollector] ✅ Collected event: ${parsed.type}/${parsed.name} (category: ${parsed.category || 'none'}, session: ${sessionId}, queue: ${this.events.length})`;
|
|
69
74
|
console.error(successMsg);
|
|
70
75
|
// Also log to stderr for better visibility in main logs
|
|
71
76
|
process.stderr.write(successMsg + '\n');
|
|
@@ -73,10 +78,11 @@ class UsageCollector {
|
|
|
73
78
|
/**
|
|
74
79
|
* Collect usage event directly (for backward compatibility with tests)
|
|
75
80
|
*/
|
|
76
|
-
collectEvent(type, name, sessionId, success = true, args) {
|
|
81
|
+
collectEvent(type, name, sessionId, success = true, args, category) {
|
|
77
82
|
const event = {
|
|
78
83
|
type,
|
|
79
84
|
name,
|
|
85
|
+
category,
|
|
80
86
|
// Use set apiKeyId if available, otherwise a placeholder.
|
|
81
87
|
// The server will override this with the correct value from the auth token.
|
|
82
88
|
apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
|
|
@@ -138,16 +144,18 @@ class UsageCollector {
|
|
|
138
144
|
parseMCPCall(toolName, args) {
|
|
139
145
|
switch (toolName) {
|
|
140
146
|
case 'get_fraim_job':
|
|
141
|
-
|
|
147
|
+
const jobPath = args.job || 'unknown';
|
|
148
|
+
const parsedJob = this.parseComponentName(jobPath);
|
|
149
|
+
return parsedJob || { type: 'job', name: jobPath };
|
|
142
150
|
case 'get_fraim_file':
|
|
143
151
|
if (args.path) {
|
|
144
|
-
return
|
|
152
|
+
return this.parseComponentName(args.path);
|
|
145
153
|
}
|
|
146
154
|
return null;
|
|
147
155
|
case 'seekMentoring':
|
|
148
|
-
return { type: 'mentoring', name: UsageCollector.resolveMentoringJobName(args) };
|
|
156
|
+
return { type: 'mentoring', name: UsageCollector.resolveMentoringJobName(args), category: 'mentoring' };
|
|
149
157
|
case 'list_fraim_jobs':
|
|
150
|
-
return { type: 'job', name: 'list' };
|
|
158
|
+
return { type: 'job', name: 'list', category: 'none' };
|
|
151
159
|
case 'fraim_connect':
|
|
152
160
|
return { type: 'session', name: 'connect' };
|
|
153
161
|
default:
|
|
@@ -226,37 +234,47 @@ class UsageCollector {
|
|
|
226
234
|
/**
|
|
227
235
|
* Parse component name from file path
|
|
228
236
|
*/
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
parseComponentName(path) {
|
|
238
|
+
if (!path || typeof path !== 'string')
|
|
239
|
+
return null;
|
|
240
|
+
// Clean path: handle backslashes, optional 'registry/' prefix
|
|
241
|
+
let cleanPath = path.replace(/\\/g, '/');
|
|
242
|
+
cleanPath = cleanPath.replace(/^(registry\/)/, '');
|
|
243
|
+
if (cleanPath.startsWith('/'))
|
|
244
|
+
cleanPath = cleanPath.substring(1);
|
|
245
|
+
const parts = cleanPath.split('/');
|
|
246
|
+
const fileName = parts[parts.length - 1];
|
|
247
|
+
// If it doesn't end in .md, it might still be a job name (e.g. from get_fraim_job)
|
|
248
|
+
if (!fileName.endsWith('.md')) {
|
|
249
|
+
return null;
|
|
238
250
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
const name = fileName.replace(/\.md$/, '');
|
|
252
|
+
const typeStr = parts[0];
|
|
253
|
+
// Match skills files
|
|
254
|
+
if (typeStr === 'skills') {
|
|
255
|
+
// Structure: skills/category/name.md or skills/name.md
|
|
256
|
+
// Category is the directory immediately following 'skills'
|
|
257
|
+
const category = parts.length > 2 ? parts[1] : undefined;
|
|
258
|
+
return { type: 'skill', name, category };
|
|
245
259
|
}
|
|
246
|
-
// Match
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
// Match job files
|
|
261
|
+
if (typeStr === 'jobs') {
|
|
262
|
+
// Structure: jobs/ai-employee/category/name.md
|
|
263
|
+
// Or: jobs/category/name.md
|
|
264
|
+
let category;
|
|
265
|
+
if (parts[1] === 'ai-employee' || parts[1] === 'ai-manager' || parts[1] === 'personalized-employee') {
|
|
266
|
+
category = parts[2]; // e.g. jobs/ai-employee/product-building/job.md -> product-building
|
|
267
|
+
}
|
|
268
|
+
else if (parts.length > 2) {
|
|
269
|
+
category = parts[1]; // e.g. jobs/legal/job.md -> legal
|
|
251
270
|
}
|
|
271
|
+
return { type: 'job', name, category };
|
|
252
272
|
}
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const baseName = fileName.replace(/\.md$/, '');
|
|
259
|
-
return { type: 'job', name: baseName };
|
|
273
|
+
// Match rule files (already defined in UsageEventType)
|
|
274
|
+
if (typeStr === 'rules') {
|
|
275
|
+
// Structure: rules/category/name.md or rules/name.md
|
|
276
|
+
const category = parts.length > 2 ? parts[1] : undefined;
|
|
277
|
+
return { type: 'rule', name, category };
|
|
260
278
|
}
|
|
261
279
|
return null;
|
|
262
280
|
}
|
package/index.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FRAIM Framework - 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 { spawnSync } = require('child_process');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Runs the CLI using either the compiled JS or the source TS via tsx
|
|
14
|
-
*/
|
|
15
|
-
function runCLI() {
|
|
16
|
-
const distPath = path.join(__dirname, 'dist', 'src', 'cli', 'fraim.js');
|
|
17
|
-
const srcPath = path.join(__dirname, 'src', 'cli', 'fraim.ts');
|
|
18
|
-
|
|
19
|
-
// 1. Check if we have a compiled version (Production / CI)
|
|
20
|
-
if (fs.existsSync(distPath)) {
|
|
21
|
-
require(distPath);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 2. Explicitly fail in production if dist/ is missing
|
|
26
|
-
if (process.env.NODE_ENV === 'production') {
|
|
27
|
-
console.error('❌ FRAIM Error: Production build (dist/) not found.');
|
|
28
|
-
console.error('In production environments, you must run the compiled version.');
|
|
29
|
-
console.error(`Expected: ${distPath}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 3. Fallback to source version using tsx (Development)
|
|
34
|
-
if (fs.existsSync(srcPath)) {
|
|
35
|
-
// We use spawnSync to run tsx so we don't have to require it in memory
|
|
36
|
-
// if it's not needed, and it handles the process arguments correctly.
|
|
37
|
-
//
|
|
38
|
-
// IMPORTANT FIX: Directory names with spaces and dashes (e.g., "FRAIM - Issue 166")
|
|
39
|
-
// cause argument parsing issues on Windows when shell: true is used.
|
|
40
|
-
// Without quoting, a path like "C:\...\FRAIM - Issue 166\src\cli\fraim.ts" gets
|
|
41
|
-
// split into multiple arguments, with the dash interpreted as a command flag,
|
|
42
|
-
// resulting in "error: unknown command '-'".
|
|
43
|
-
//
|
|
44
|
-
// Solution: On Windows with shell: true, quote paths containing spaces.
|
|
45
|
-
// On Unix with shell: false, pass the path unquoted (spawnSync handles it correctly).
|
|
46
|
-
const isWindows = process.platform === 'win32';
|
|
47
|
-
|
|
48
|
-
// On Windows with shell, quote paths with spaces to prevent shell misinterpretation
|
|
49
|
-
// On Unix without shell, pass path as-is (spawnSync handles spaces correctly)
|
|
50
|
-
const processedSrcPath = (isWindows && srcPath.includes(' '))
|
|
51
|
-
? `"${srcPath}"`
|
|
52
|
-
: srcPath;
|
|
53
|
-
|
|
54
|
-
const result = spawnSync(
|
|
55
|
-
'npx',
|
|
56
|
-
['tsx', processedSrcPath, ...process.argv.slice(2)],
|
|
57
|
-
{
|
|
58
|
-
stdio: 'inherit',
|
|
59
|
-
shell: isWindows, // Windows needs shell for npx, Unix doesn't
|
|
60
|
-
windowsHide: true
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
process.exit(result.status || 0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.error('❌ FRAIM Error: Could not find CLI entry point.');
|
|
67
|
-
console.error('Expected one of:');
|
|
68
|
-
console.error(` - ${distPath}`);
|
|
69
|
-
console.error(` - ${srcPath}`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Global programmatic exports
|
|
74
|
-
module.exports = {
|
|
75
|
-
FRAIM_INFO: {
|
|
76
|
-
name: 'FRAIM',
|
|
77
|
-
version: '2.0.98',
|
|
78
|
-
repository: 'https://github.com/mathursrus/FRAIM'
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// If this file is run directly (via npx or global link), run the CLI
|
|
83
|
-
if (require.main === module) {
|
|
84
|
-
runCLI();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FRAIM Framework - 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 { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs the CLI using either the compiled JS or the source TS via tsx
|
|
14
|
+
*/
|
|
15
|
+
function runCLI() {
|
|
16
|
+
const distPath = path.join(__dirname, 'dist', 'src', 'cli', 'fraim.js');
|
|
17
|
+
const srcPath = path.join(__dirname, 'src', 'cli', 'fraim.ts');
|
|
18
|
+
|
|
19
|
+
// 1. Check if we have a compiled version (Production / CI)
|
|
20
|
+
if (fs.existsSync(distPath)) {
|
|
21
|
+
require(distPath);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Explicitly fail in production if dist/ is missing
|
|
26
|
+
if (process.env.NODE_ENV === 'production') {
|
|
27
|
+
console.error('❌ FRAIM Error: Production build (dist/) not found.');
|
|
28
|
+
console.error('In production environments, you must run the compiled version.');
|
|
29
|
+
console.error(`Expected: ${distPath}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Fallback to source version using tsx (Development)
|
|
34
|
+
if (fs.existsSync(srcPath)) {
|
|
35
|
+
// We use spawnSync to run tsx so we don't have to require it in memory
|
|
36
|
+
// if it's not needed, and it handles the process arguments correctly.
|
|
37
|
+
//
|
|
38
|
+
// IMPORTANT FIX: Directory names with spaces and dashes (e.g., "FRAIM - Issue 166")
|
|
39
|
+
// cause argument parsing issues on Windows when shell: true is used.
|
|
40
|
+
// Without quoting, a path like "C:\...\FRAIM - Issue 166\src\cli\fraim.ts" gets
|
|
41
|
+
// split into multiple arguments, with the dash interpreted as a command flag,
|
|
42
|
+
// resulting in "error: unknown command '-'".
|
|
43
|
+
//
|
|
44
|
+
// Solution: On Windows with shell: true, quote paths containing spaces.
|
|
45
|
+
// On Unix with shell: false, pass the path unquoted (spawnSync handles it correctly).
|
|
46
|
+
const isWindows = process.platform === 'win32';
|
|
47
|
+
|
|
48
|
+
// On Windows with shell, quote paths with spaces to prevent shell misinterpretation
|
|
49
|
+
// On Unix without shell, pass path as-is (spawnSync handles spaces correctly)
|
|
50
|
+
const processedSrcPath = (isWindows && srcPath.includes(' '))
|
|
51
|
+
? `"${srcPath}"`
|
|
52
|
+
: srcPath;
|
|
53
|
+
|
|
54
|
+
const result = spawnSync(
|
|
55
|
+
'npx',
|
|
56
|
+
['tsx', processedSrcPath, ...process.argv.slice(2)],
|
|
57
|
+
{
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
shell: isWindows, // Windows needs shell for npx, Unix doesn't
|
|
60
|
+
windowsHide: true
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
process.exit(result.status || 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.error('❌ FRAIM Error: Could not find CLI entry point.');
|
|
67
|
+
console.error('Expected one of:');
|
|
68
|
+
console.error(` - ${distPath}`);
|
|
69
|
+
console.error(` - ${srcPath}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Global programmatic exports
|
|
74
|
+
module.exports = {
|
|
75
|
+
FRAIM_INFO: {
|
|
76
|
+
name: 'FRAIM',
|
|
77
|
+
version: '2.0.98',
|
|
78
|
+
repository: 'https://github.com/mathursrus/FRAIM'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// If this file is run directly (via npx or global link), run the CLI
|
|
83
|
+
if (require.main === module) {
|
|
84
|
+
runCLI();
|
|
85
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.101",
|
|
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": {
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"test-all": "npm run test && npm run test:isolated && npm run test:ui",
|
|
17
17
|
"test": "node scripts/test-with-server.js",
|
|
18
18
|
"test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
|
|
19
|
-
"test:smoke": "node scripts/test-with-server.js
|
|
19
|
+
"test:smoke": "node scripts/test-with-server.js --tags=smoke",
|
|
20
20
|
"test:stripe": "node scripts/test-with-server.js tests/test-stripe-payment-complete.ts",
|
|
21
21
|
"test:stripe:ui": "playwright test tests/ui/test-payment-ui.spec.ts",
|
|
22
|
+
"test:perf": "node scripts/test-with-server.js tests/performance/analytics-perf.ts",
|
|
22
23
|
"test:ui": "playwright test",
|
|
23
24
|
"test:ui:headed": "playwright test --headed",
|
|
24
25
|
"start:fraim": "tsx src/fraim-mcp-server.ts",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"serve:website": "node fraim-pro/serve.js",
|
|
27
28
|
"watch:fraimlogs": "tsx scripts/watch-fraim-logs.ts > prodlogs.log 2>&1",
|
|
28
29
|
"manage-keys": "tsx scripts/fraim/manage-keys.ts",
|
|
30
|
+
"manage-teams": "tsx scripts/fraim/manage-teams.ts",
|
|
29
31
|
"partner-discounts": "tsx scripts/fraim/manage-partner-discounts.ts",
|
|
30
32
|
"fix-key": "tsx scripts/fraim/fix-expired-key.ts",
|
|
31
33
|
"setup-stripe-webhook": "tsx scripts/fraim/setup-stripe-webhook.ts",
|
|
@@ -89,6 +91,7 @@
|
|
|
89
91
|
"@types/cors": "^2.8.19",
|
|
90
92
|
"@types/express": "^5.0.6",
|
|
91
93
|
"@types/node": "^20.0.0",
|
|
94
|
+
"@types/node-fetch": "^2.6.13",
|
|
92
95
|
"@types/prompts": "^2.4.9",
|
|
93
96
|
"@types/semver": "^7.7.1",
|
|
94
97
|
"fast-glob": "^3.3.3",
|
|
@@ -119,6 +122,7 @@
|
|
|
119
122
|
"access": "public"
|
|
120
123
|
},
|
|
121
124
|
"dependencies": {
|
|
125
|
+
"@octokit/rest": "^22.0.1",
|
|
122
126
|
"adm-zip": "^0.5.16",
|
|
123
127
|
"axios": "^1.7.0",
|
|
124
128
|
"chalk": "4.1.2",
|
|
@@ -128,6 +132,7 @@
|
|
|
128
132
|
"express": "^5.2.1",
|
|
129
133
|
"mongodb": "^7.0.0",
|
|
130
134
|
"node-edge-tts": "^1.2.10",
|
|
135
|
+
"nodemailer": "^8.0.3",
|
|
131
136
|
"prompts": "^2.4.2",
|
|
132
137
|
"resend": "^6.9.3",
|
|
133
138
|
"semver": "^7.7.4",
|