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 = fs_1.default.existsSync(path_1.default.join(__dirname, '..', '..', '..', 'registry'))
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 = path_1.default.join(__dirname, '..', '..', '..', 'labels.json');
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
- ...tokens
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. Tell your AI agent: "Onboard this project"'));
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
- - Use stub filenames and summaries to discover what to invoke. Once you identify a relevant job, call \`get_fraim_job({ job: "<job-name>" })\`.
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
- const possiblePaths = [
14
- path_1.default.join(__dirname, '../../../package.json'), // Local dev (src)
15
- path_1.default.join(__dirname, '../../../../package.json'), // Dist (dist/src)
16
- path_1.default.join(process.cwd(), 'package.json') // Fallback to CWD
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 '1.0.0'; // Fallback
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.apiKeyId = null;
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 API key ID for this session
34
+ * Set the user ID for this session
39
35
  */
40
- setApiKeyId(apiKeyId) {
41
- this.apiKeyId = apiKeyId;
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 apiKeyId if available, otherwise a placeholder.
65
- // The server will override this with the correct ID from the authenticated API key.
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
- // Use set apiKeyId if available, otherwise a placeholder.
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.101",
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": {