fraim-framework 2.0.101 → 2.0.103
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.
|
@@ -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.initProjectCommand = exports.runInitProject = void 0;
|
|
6
|
+
exports.initProjectCommand = exports.runInitProject = exports.findProjectFile = void 0;
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -70,12 +70,41 @@ const checkGlobalSetup = () => {
|
|
|
70
70
|
return { exists: true, mode: 'integrated', tokens: {} };
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
+
// Robust path resolution utility - walks up directory tree to find target
|
|
74
|
+
const findProjectFile = (filename) => {
|
|
75
|
+
let currentDir = __dirname;
|
|
76
|
+
// Walk up the directory tree to find the target file/directory
|
|
77
|
+
for (let i = 0; i < 10; i++) { // Limit to prevent infinite loop
|
|
78
|
+
const targetPath = path_1.default.join(currentDir, filename);
|
|
79
|
+
if (fs_1.default.existsSync(targetPath)) {
|
|
80
|
+
return targetPath;
|
|
81
|
+
}
|
|
82
|
+
const parentDir = path_1.default.dirname(currentDir);
|
|
83
|
+
if (parentDir === currentDir)
|
|
84
|
+
break; // Reached root
|
|
85
|
+
currentDir = parentDir;
|
|
86
|
+
}
|
|
87
|
+
// Fallback: try from process.cwd()
|
|
88
|
+
const cwdTarget = path_1.default.join(process.cwd(), filename);
|
|
89
|
+
if (fs_1.default.existsSync(cwdTarget)) {
|
|
90
|
+
return cwdTarget;
|
|
91
|
+
}
|
|
92
|
+
// Last resort: use relative path from __dirname
|
|
93
|
+
return path_1.default.join(__dirname, '..', '..', '..', filename);
|
|
94
|
+
};
|
|
95
|
+
exports.findProjectFile = findProjectFile;
|
|
96
|
+
;
|
|
73
97
|
const installGitHubWorkflows = (projectRoot) => {
|
|
74
98
|
const workflowsDir = path_1.default.join(projectRoot, '.github', 'workflows');
|
|
75
|
-
const registryDir =
|
|
76
|
-
? path_1.default.join(__dirname, '..', '..', '..', 'registry')
|
|
77
|
-
: path_1.default.join(__dirname, '..', '..', 'registry');
|
|
99
|
+
const registryDir = (0, exports.findProjectFile)('registry');
|
|
78
100
|
const sourceDir = path_1.default.join(registryDir, 'github', 'workflows');
|
|
101
|
+
if (!fs_1.default.existsSync(sourceDir)) {
|
|
102
|
+
console.log(chalk_1.default.yellow(`Warning: GitHub workflows source directory not found: ${sourceDir}`));
|
|
103
|
+
console.log(chalk_1.default.gray(`Registry directory: ${registryDir}`));
|
|
104
|
+
console.log(chalk_1.default.gray(`Current __dirname: ${__dirname}`));
|
|
105
|
+
console.log(chalk_1.default.gray(`Process cwd: ${process.cwd()}`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
79
108
|
if (!fs_1.default.existsSync(workflowsDir)) {
|
|
80
109
|
fs_1.default.mkdirSync(workflowsDir, { recursive: true });
|
|
81
110
|
}
|
|
@@ -101,9 +130,10 @@ const createGitHubLabels = (projectRoot) => {
|
|
|
101
130
|
console.log(chalk_1.default.gray('Install gh CLI to enable automatic label creation: https://cli.github.com/'));
|
|
102
131
|
return;
|
|
103
132
|
}
|
|
104
|
-
const labelsPath =
|
|
133
|
+
const labelsPath = (0, exports.findProjectFile)('labels.json');
|
|
105
134
|
if (!fs_1.default.existsSync(labelsPath)) {
|
|
106
135
|
console.log(chalk_1.default.yellow('labels.json not found. Skipping label creation.'));
|
|
136
|
+
console.log(chalk_1.default.gray(`Searched from: ${__dirname}`));
|
|
107
137
|
return;
|
|
108
138
|
}
|
|
109
139
|
try {
|
|
@@ -146,6 +146,33 @@ const promptForMode = async () => {
|
|
|
146
146
|
}
|
|
147
147
|
return response.mode;
|
|
148
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Sanitize a token by removing control characters that cause JSON serialization issues
|
|
151
|
+
*/
|
|
152
|
+
const sanitizeToken = (token) => {
|
|
153
|
+
if (!token)
|
|
154
|
+
return token;
|
|
155
|
+
// Remove control characters (0x00-0x1F and 0x7F) that cause JSON escaping issues
|
|
156
|
+
// These characters are not valid in API tokens and are likely copy-paste artifacts
|
|
157
|
+
return token.replace(/[\x00-\x1F\x7F]/g, '');
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Sanitize all tokens in a ProviderTokens object
|
|
161
|
+
*/
|
|
162
|
+
const sanitizeTokens = (tokens) => {
|
|
163
|
+
const sanitized = {};
|
|
164
|
+
Object.entries(tokens).forEach(([providerId, token]) => {
|
|
165
|
+
if (token) {
|
|
166
|
+
const originalToken = token;
|
|
167
|
+
const sanitizedToken = sanitizeToken(token);
|
|
168
|
+
if (originalToken !== sanitizedToken) {
|
|
169
|
+
console.log(chalk_1.default.yellow(`⚠️ Sanitized ${providerId} token: removed ${originalToken.length - sanitizedToken.length} control character(s)`));
|
|
170
|
+
}
|
|
171
|
+
sanitized[providerId] = sanitizedToken;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return sanitized;
|
|
175
|
+
};
|
|
149
176
|
const saveGlobalConfig = (fraimKey, mode, tokens, configs) => {
|
|
150
177
|
const globalConfigDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
151
178
|
const globalConfigPath = path_1.default.join(globalConfigDir, 'config.json');
|
|
@@ -162,6 +189,8 @@ const saveGlobalConfig = (fraimKey, mode, tokens, configs) => {
|
|
|
162
189
|
// Ignore parse errors, will create new config
|
|
163
190
|
}
|
|
164
191
|
}
|
|
192
|
+
// Sanitize tokens before saving to prevent JSON serialization issues
|
|
193
|
+
const sanitizedTokens = sanitizeTokens(tokens);
|
|
165
194
|
// Merge provider configs (e.g., jiraConfig)
|
|
166
195
|
const providerConfigs = { ...(existingConfig.providerConfigs || {}) };
|
|
167
196
|
Object.entries(configs).forEach(([providerId, config]) => {
|
|
@@ -176,7 +205,7 @@ const saveGlobalConfig = (fraimKey, mode, tokens, configs) => {
|
|
|
176
205
|
mode: mode,
|
|
177
206
|
tokens: {
|
|
178
207
|
...(existingConfig.tokens || {}),
|
|
179
|
-
...
|
|
208
|
+
...sanitizedTokens
|
|
180
209
|
},
|
|
181
210
|
providerConfigs,
|
|
182
211
|
configuredAt: new Date().toISOString(),
|
|
@@ -594,7 +623,7 @@ const runSetup = async (options) => {
|
|
|
594
623
|
console.log(chalk_1.default.cyan('\n📝 For future projects:'));
|
|
595
624
|
console.log(chalk_1.default.cyan(' 1. cd into any project directory'));
|
|
596
625
|
console.log(chalk_1.default.cyan(' 2. Run: fraim init-project'));
|
|
597
|
-
console.log(chalk_1.default.cyan(' 3.
|
|
626
|
+
console.log(chalk_1.default.cyan(' 3. Ask your AI agent: "FRAIM was just installed. Read the FRAIM docs, explain what it can do for me, then run project-onboarding."'));
|
|
598
627
|
if (mode === 'integrated') {
|
|
599
628
|
const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
|
|
600
629
|
const unconfiguredProviders = allProviderIds.filter(id => !tokens[id]);
|
|
@@ -10,6 +10,7 @@ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
|
10
10
|
const START_MARKER = '<!-- FRAIM_AGENT_ADAPTER_START -->';
|
|
11
11
|
const END_MARKER = '<!-- FRAIM_AGENT_ADAPTER_END -->';
|
|
12
12
|
const CURSOR_RULE_PATH = path_1.default.join('.cursor', 'rules', 'fraim.mdc');
|
|
13
|
+
const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fraim.md');
|
|
13
14
|
const CURSOR_FRONTMATTER = `---
|
|
14
15
|
description: FRAIM discovery and execution contract
|
|
15
16
|
alwaysApply: true
|
|
@@ -59,9 +60,14 @@ This repository uses FRAIM.
|
|
|
59
60
|
- Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
|
|
60
61
|
- Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
|
|
61
62
|
- Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
|
|
62
|
-
-
|
|
63
|
+
- Before acting on any user request, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections to match the request to the right job.
|
|
64
|
+
- Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
|
|
63
65
|
- For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
|
|
64
66
|
- Read \`${projectRulesPath}\` if it exists before doing work.
|
|
67
|
+
- When users ask for next step recommendations, use recommend-next-job skill under \`${employeeSkillsPath}/\` to gather context before suggesting jobs.
|
|
68
|
+
|
|
69
|
+
> [!IMPORTANT]
|
|
70
|
+
> **Job stubs are for discovery only.** When a user @mentions or references any file under \`${employeeJobsPath}/\` or \`${managerJobsPath}/\`, do NOT attempt to execute the job from the stub content. The stub only shows intent and phase names. Always call \`get_fraim_job({ job: "<job-name>" })\` first to get the full phased instructions before doing any work.
|
|
65
71
|
`);
|
|
66
72
|
const cursorManagedBody = buildManagedSection(`
|
|
67
73
|
Use FRAIM as the repo's execution framework.
|
|
@@ -72,6 +78,7 @@ Use FRAIM as the repo's execution framework.
|
|
|
72
78
|
- Rules are always-on constraints.
|
|
73
79
|
- Repo-specific overrides and learnings under \`${personalizedRootPath}/\` take precedence.
|
|
74
80
|
- Choose a relevant job from the stubs, then call \`get_fraim_job(...)\` for the full phased instructions.
|
|
81
|
+
- **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
75
82
|
`);
|
|
76
83
|
const copilotBody = buildManagedSection(`
|
|
77
84
|
## FRAIM
|
|
@@ -82,6 +89,7 @@ Use FRAIM as the repo's execution framework.
|
|
|
82
89
|
- FRAIM rules are always-on constraints and conventions.
|
|
83
90
|
- Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
|
|
84
91
|
- Use the stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
|
|
92
|
+
- **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
|
|
85
93
|
`);
|
|
86
94
|
const fraimReadme = `# FRAIM Catalog
|
|
87
95
|
|
|
@@ -94,13 +102,26 @@ This directory is the repository-visible FRAIM surface.
|
|
|
94
102
|
- \`personalized-employee/\`: repo-specific overrides and learnings
|
|
95
103
|
|
|
96
104
|
Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools.
|
|
105
|
+
`;
|
|
106
|
+
const claudeCommand = `The user wants to run FRAIM. The requested job or topic is: $ARGUMENTS
|
|
107
|
+
|
|
108
|
+
Follow this process:
|
|
109
|
+
|
|
110
|
+
1. **If no argument was given** (the line above ends with ": "): scan all stub files under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\`. List each by filename and its Intent line. Ask the user which job they want to run, then proceed to step 2.
|
|
111
|
+
|
|
112
|
+
2. **Find the job**: search \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` for a stub whose filename (without \`.md\`) matches or closely resembles the argument. Read the stub's Intent/Outcome to confirm it matches what the user wants.
|
|
113
|
+
|
|
114
|
+
3. **Load the full job**: call \`get_fraim_job({ job: "<matched-job-name>" })\` — never execute from stub content.
|
|
115
|
+
|
|
116
|
+
4. **Execute**: follow the phased instructions returned by \`get_fraim_job\`, using \`seekMentoring\` at phase transitions where indicated.
|
|
97
117
|
`;
|
|
98
118
|
return [
|
|
99
119
|
{ path: 'AGENTS.md', content: markdownBody },
|
|
100
120
|
{ path: 'CLAUDE.md', content: markdownBody },
|
|
101
121
|
{ path: path_1.default.join('.github', 'copilot-instructions.md'), content: copilotBody },
|
|
102
122
|
{ path: CURSOR_RULE_PATH, content: cursorManagedBody },
|
|
103
|
-
{ path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme }
|
|
123
|
+
{ path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme },
|
|
124
|
+
{ path: CLAUDE_FRAIM_COMMAND_PATH, content: claudeCommand }
|
|
104
125
|
];
|
|
105
126
|
}
|
|
106
127
|
function ensureAgentAdapterFiles(projectRoot) {
|
|
@@ -114,7 +135,7 @@ function ensureAgentAdapterFiles(projectRoot) {
|
|
|
114
135
|
const existing = fs_1.default.existsSync(fullPath) ? fs_1.default.readFileSync(fullPath, 'utf8') : '';
|
|
115
136
|
const next = file.path === CURSOR_RULE_PATH
|
|
116
137
|
? mergeCursorRule(existing, file.content)
|
|
117
|
-
: file.path.endsWith('README.md')
|
|
138
|
+
: file.path.endsWith('README.md') || file.path === CLAUDE_FRAIM_COMMAND_PATH
|
|
118
139
|
? file.content
|
|
119
140
|
: mergeManagedSection(existing, file.content);
|
|
120
141
|
if (existing !== next) {
|
|
@@ -10,12 +10,11 @@ function getFraimVersion() {
|
|
|
10
10
|
// Try reliable paths to find package.json relative to this file
|
|
11
11
|
// locally: src/cli/utils/version-utils.ts -> package.json is ../../../package.json
|
|
12
12
|
// dist: dist/src/cli/utils/version-utils.js -> package.json is ../../../../package.json
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for (const pkgPath of possiblePaths) {
|
|
13
|
+
// Traverse up from __dirname until we find fraim-framework's package.json.
|
|
14
|
+
// Fixed relative paths break when npx cache layouts differ across OS/npm versions.
|
|
15
|
+
let dir = __dirname;
|
|
16
|
+
for (let i = 0; i < 10; i++) {
|
|
17
|
+
const pkgPath = path_1.default.join(dir, 'package.json');
|
|
19
18
|
if (fs_1.default.existsSync(pkgPath)) {
|
|
20
19
|
try {
|
|
21
20
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -27,6 +26,10 @@ function getFraimVersion() {
|
|
|
27
26
|
// Ignore parsing errors
|
|
28
27
|
}
|
|
29
28
|
}
|
|
29
|
+
const parent = path_1.default.dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
break; // Reached filesystem root
|
|
32
|
+
dir = parent;
|
|
30
33
|
}
|
|
31
|
-
return '
|
|
34
|
+
return 'unknown'; // Do not return a fake version — unknown is safer than misleading
|
|
32
35
|
}
|
|
@@ -4,11 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.UsageCollector = void 0;
|
|
7
|
-
const mongodb_1 = require("mongodb");
|
|
8
7
|
const axios_1 = __importDefault(require("axios"));
|
|
9
|
-
// A placeholder ObjectId used when the real API key ID is not yet known.
|
|
10
|
-
// The server will override this with the correct ID from the authenticated API key.
|
|
11
|
-
const PLACEHOLDER_API_KEY_ID = new mongodb_1.ObjectId('000000000000000000000000');
|
|
12
8
|
/**
|
|
13
9
|
* UsageCollector is responsible for collecting usage events from MCP tools
|
|
14
10
|
* and formatting them for the analytics system.
|
|
@@ -16,7 +12,7 @@ const PLACEHOLDER_API_KEY_ID = new mongodb_1.ObjectId('000000000000000000000000'
|
|
|
16
12
|
class UsageCollector {
|
|
17
13
|
constructor() {
|
|
18
14
|
this.events = [];
|
|
19
|
-
this.
|
|
15
|
+
this.userId = null;
|
|
20
16
|
}
|
|
21
17
|
static resolveMentoringJobName(args) {
|
|
22
18
|
if (!args || typeof args !== 'object') {
|
|
@@ -35,10 +31,10 @@ class UsageCollector {
|
|
|
35
31
|
return 'unknown';
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
|
-
* Set the
|
|
34
|
+
* Set the user ID for this session
|
|
39
35
|
*/
|
|
40
|
-
|
|
41
|
-
this.
|
|
36
|
+
setUserId(userId) {
|
|
37
|
+
this.userId = userId;
|
|
42
38
|
}
|
|
43
39
|
/**
|
|
44
40
|
* Collect MCP tool call event
|
|
@@ -61,9 +57,8 @@ class UsageCollector {
|
|
|
61
57
|
const event = {
|
|
62
58
|
type: parsed.type,
|
|
63
59
|
name: parsed.name,
|
|
64
|
-
// Use set
|
|
65
|
-
|
|
66
|
-
apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
|
|
60
|
+
// Use set userId if available; the server will override with the authenticated userId.
|
|
61
|
+
userId: this.userId || 'unknown',
|
|
67
62
|
sessionId,
|
|
68
63
|
success,
|
|
69
64
|
category: parsed.category,
|
|
@@ -83,9 +78,7 @@ class UsageCollector {
|
|
|
83
78
|
type,
|
|
84
79
|
name,
|
|
85
80
|
category,
|
|
86
|
-
|
|
87
|
-
// The server will override this with the correct value from the auth token.
|
|
88
|
-
apiKeyId: this.apiKeyId || PLACEHOLDER_API_KEY_ID,
|
|
81
|
+
userId: this.userId || 'unknown',
|
|
89
82
|
sessionId,
|
|
90
83
|
success,
|
|
91
84
|
args
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.103",
|
|
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": {
|