fraim-framework 2.0.123 → 2.0.126

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/index.js CHANGED
@@ -1,85 +1,85 @@
1
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();
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.123",
3
+ "version": "2.0.126",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "manage-teams": "tsx scripts/fraim/manage-teams.ts",
33
33
  "partner-discounts": "tsx scripts/fraim/manage-partner-discounts.ts",
34
34
  "fix-key": "tsx scripts/fraim/fix-expired-key.ts",
35
+ "backfill:persona-entitlements": "tsx scripts/backfill-persona-entitlements.ts",
35
36
  "setup-stripe-webhook": "tsx scripts/fraim/setup-stripe-webhook.ts",
36
37
  "view-signups": "tsx scripts/view-signups.ts",
37
38
  "fraim:init": "npm run build && node index.js init",
@@ -126,6 +127,9 @@
126
127
  "publishConfig": {
127
128
  "access": "public"
128
129
  },
130
+ "overrides": {
131
+ "uuid": "^14.0.0"
132
+ },
129
133
  "dependencies": {
130
134
  "@octokit/rest": "^22.0.1",
131
135
  "adm-zip": "^0.5.16",
@@ -1,83 +0,0 @@
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;
@@ -1,152 +0,0 @@
1
- "use strict";
2
- /**
3
- * Prometheus Scraper for Claude Code OTel Token Usage Metrics
4
- *
5
- * Scrapes Claude Code's local Prometheus endpoint to capture cumulative
6
- * token usage counters. Returns raw snapshot values — delta computation
7
- * happens server-side per architecture constraint 15.2.
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.parsePrometheusText = parsePrometheusText;
11
- exports.extractTokenSnapshot = extractTokenSnapshot;
12
- exports.scrapeTokenSnapshot = scrapeTokenSnapshot;
13
- /** Candidate Prometheus endpoint ports (OTel SDK defaults) */
14
- const CANDIDATE_PORTS = [9464, 8888];
15
- /** Candidate metric names for token usage (OTel dots→underscores, optional suffixes) */
16
- const TOKEN_METRIC_CANDIDATES = [
17
- 'claude_code_token_usage_total',
18
- 'claude_code_token_usage',
19
- 'claude_code_token_usage_tokens_total',
20
- 'claude_code_token_usage_tokens',
21
- ];
22
- /** Candidate metric names for cost usage */
23
- const COST_METRIC_CANDIDATES = [
24
- 'claude_code_cost_usage_total',
25
- 'claude_code_cost_usage',
26
- 'claude_code_cost_usage_usd_total',
27
- 'claude_code_cost_usage_usd',
28
- ];
29
- /**
30
- * Parse Prometheus text exposition format into structured metrics.
31
- *
32
- * Format per line: metric_name{label1="val1",label2="val2"} value [timestamp]
33
- * Lines starting with # are comments (HELP, TYPE). Empty lines are skipped.
34
- */
35
- function parsePrometheusText(text) {
36
- const results = [];
37
- for (const line of text.split('\n')) {
38
- if (line.startsWith('#') || line.trim() === '')
39
- continue;
40
- // Match: metric_name{labels} value
41
- const labeledMatch = line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)\{([^}]*)\}\s+([\d.eE+-]+)/);
42
- if (labeledMatch) {
43
- const [, name, labelsStr, valueStr] = labeledMatch;
44
- const labels = {};
45
- for (const pair of labelsStr.matchAll(/(\w+)="([^"]*)"/g)) {
46
- labels[pair[1]] = pair[2];
47
- }
48
- results.push({ name, labels, value: parseFloat(valueStr) });
49
- continue;
50
- }
51
- // Match: metric_name value (no labels)
52
- const noLabelMatch = line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)\s+([\d.eE+-]+)/);
53
- if (noLabelMatch) {
54
- const [, name, valueStr] = noLabelMatch;
55
- results.push({ name, labels: {}, value: parseFloat(valueStr) });
56
- }
57
- }
58
- return results;
59
- }
60
- /**
61
- * Extract a TokenSnapshot from parsed Prometheus metrics.
62
- * Tries multiple candidate metric names for resilience to SDK version changes.
63
- */
64
- function extractTokenSnapshot(metrics) {
65
- const snapshot = {
66
- inputTokens: 0,
67
- outputTokens: 0,
68
- cacheReadTokens: 0,
69
- cacheCreationTokens: 0,
70
- costUsd: 0,
71
- claudeSessionId: null,
72
- model: null,
73
- capturedAt: new Date(),
74
- };
75
- for (const m of metrics) {
76
- if (TOKEN_METRIC_CANDIDATES.includes(m.name)) {
77
- const type = m.labels['type'];
78
- switch (type) {
79
- case 'input':
80
- snapshot.inputTokens = m.value;
81
- break;
82
- case 'output':
83
- snapshot.outputTokens = m.value;
84
- break;
85
- case 'cacheRead':
86
- snapshot.cacheReadTokens = m.value;
87
- break;
88
- case 'cacheCreation':
89
- snapshot.cacheCreationTokens = m.value;
90
- break;
91
- }
92
- snapshot.claudeSessionId = m.labels['session_id'] || snapshot.claudeSessionId;
93
- snapshot.model = m.labels['model'] || snapshot.model;
94
- }
95
- if (COST_METRIC_CANDIDATES.includes(m.name)) {
96
- snapshot.costUsd += m.value;
97
- snapshot.claudeSessionId = m.labels['session_id'] || snapshot.claudeSessionId;
98
- }
99
- }
100
- return snapshot;
101
- }
102
- /**
103
- * Attempt to fetch Prometheus metrics from a candidate endpoint.
104
- * Returns response text or null on failure.
105
- */
106
- async function tryFetch(port, timeoutMs) {
107
- try {
108
- const controller = new AbortController();
109
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
110
- const resp = await fetch(`http://localhost:${port}/metrics`, {
111
- signal: controller.signal,
112
- });
113
- clearTimeout(timeout);
114
- if (resp.ok) {
115
- return await resp.text();
116
- }
117
- return null;
118
- }
119
- catch {
120
- return null;
121
- }
122
- }
123
- /**
124
- * Scrape Claude Code's Prometheus endpoint and return a TokenSnapshot.
125
- *
126
- * Returns null if:
127
- * - Prometheus endpoint is not available (OTel not enabled)
128
- * - Scrape times out (>2s by default)
129
- * - No matching token metrics found
130
- *
131
- * @param log Optional logging function for debug output
132
- */
133
- async function scrapeTokenSnapshot(log) {
134
- const configuredPort = process.env.FRAIM_PROMETHEUS_PORT;
135
- const ports = configuredPort ? [parseInt(configuredPort, 10)] : CANDIDATE_PORTS;
136
- const timeoutMs = 2000;
137
- for (const port of ports) {
138
- const text = await tryFetch(port, timeoutMs);
139
- if (text && text.includes('#')) {
140
- const metrics = parsePrometheusText(text);
141
- const snapshot = extractTokenSnapshot(metrics);
142
- // Only return if we actually found token metrics
143
- if (snapshot.inputTokens > 0 || snapshot.outputTokens > 0) {
144
- log?.(`Captured token snapshot from localhost:${port} (input=${snapshot.inputTokens}, output=${snapshot.outputTokens})`);
145
- return snapshot;
146
- }
147
- log?.(`Prometheus endpoint found at localhost:${port} but no token metrics matched`);
148
- }
149
- }
150
- log?.('No Prometheus endpoint available — token snapshot skipped');
151
- return null;
152
- }