@rockcarver/frodo-cli 4.0.0-50 → 4.0.0-51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rockcarver/frodo-cli",
3
- "version": "4.0.0-50",
3
+ "version": "4.0.0-51",
4
4
  "type": "module",
5
5
  "description": "A command line interface to manage ForgeRock Identity Cloud tenants, ForgeOps deployments, and classic deployments.",
6
6
  "keywords": [
@@ -37,6 +37,14 @@
37
37
  "test:serial": "FRODO_TEST=1 NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest --silent --runInBand",
38
38
  "test:debug": "FRODO_TEST=1 NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest --silent=false --json --outputFile=./testResults.json",
39
39
  "test:update": "FRODO_TEST=1 NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest --silent=false --updateSnapshot",
40
+ "mcp:batch1": "node tools/mcp-agentic-batch-run.mjs --output docs/mcp-agentic-run-log.batch1.json",
41
+ "mcp:batch2": "node tools/mcp-agentic-batch-run.mjs --batch batch2 --output docs/mcp-agentic-run-log.batch2.json",
42
+ "mcp:batch3": "node tools/mcp-agentic-batch-run.mjs --batch batch3 --output docs/mcp-agentic-run-log.batch3.json",
43
+ "mcp:batch4": "node tools/mcp-agentic-batch-run.mjs --batch batch4 --output docs/mcp-agentic-run-log.batch4.json",
44
+ "mcp:score": "node tools/mcp-agentic-score.mjs",
45
+ "mcp:assess": "node tools/mcp-agentic-assess.mjs",
46
+ "mcp:test-aiagents": "node tools/mcp-aiagent-introspection-test.mjs",
47
+ "mcp:test-oauth2-mayact": "node tools/mcp-oauth2-mayact-update-test.mjs",
40
48
  "lint": "eslint --cache --cache-location .eslintcache \"src/**/*.ts\"",
41
49
  "lint:fix": "eslint --cache --cache-location .eslintcache --fix \"src/**/*.ts\"",
42
50
  "build": "npm run build:binary",
@@ -96,7 +104,7 @@
96
104
  },
97
105
  "devDependencies": {
98
106
  "@modelcontextprotocol/sdk": "^1.29.0",
99
- "@rockcarver/frodo-lib": "4.0.0-39",
107
+ "@rockcarver/frodo-lib": "4.0.0-46",
100
108
  "@types/colors": "^1.2.1",
101
109
  "@types/fs-extra": "^11.0.1",
102
110
  "@types/jest": "^29.2.3",
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ const ROOT = process.cwd();
7
+ const TEST_DIR = path.join(ROOT, 'test/e2e');
8
+ const SNAP_DIR = path.join(ROOT, 'test/e2e/__snapshots__');
9
+ const MOCKS_DIR = path.join(ROOT, 'test/e2e/mocks');
10
+
11
+ const TEST_FILES = fs
12
+ .readdirSync(TEST_DIR)
13
+ .filter((f) => (f.includes('agent') || f === 'config-manager-export-oauth2-agents.e2e.test.js') && f.endsWith('.e2e.test.js'))
14
+ .sort();
15
+
16
+ const ERROR_PATTERNS = [
17
+ /\[Polly\].*Recording for the following request is not found/i,
18
+ /Error getting tokens/i,
19
+ /Command failed:/i,
20
+ /TypeError:/,
21
+ /ReferenceError:/,
22
+ /SyntaxError:/,
23
+ /ENOENT:/,
24
+ /Error exporting/i,
25
+ ];
26
+
27
+ function walkDirs(dir, predicate) {
28
+ const out = [];
29
+ if (!fs.existsSync(dir)) return out;
30
+ const stack = [dir];
31
+ while (stack.length) {
32
+ const cur = stack.pop();
33
+ for (const entry of fs.readdirSync(cur)) {
34
+ const full = path.join(cur, entry);
35
+ const st = fs.statSync(full);
36
+ if (st.isDirectory()) {
37
+ if (predicate(full, entry)) out.push(full);
38
+ stack.push(full);
39
+ }
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function countHarFiles(dir) {
46
+ let count = 0;
47
+ const stack = [dir];
48
+ while (stack.length) {
49
+ const cur = stack.pop();
50
+ for (const entry of fs.readdirSync(cur)) {
51
+ const full = path.join(cur, entry);
52
+ const st = fs.statSync(full);
53
+ if (st.isDirectory()) stack.push(full);
54
+ else if (entry.endsWith('.har')) count += 1;
55
+ }
56
+ }
57
+ return count;
58
+ }
59
+
60
+ function parseCommands(testText) {
61
+ return [...testText.matchAll(/const\s+CMD\s*=\s*`([^`]+)`/g)].map((m) => m[1]);
62
+ }
63
+
64
+ function getSuiteStem(testFile) {
65
+ return testFile.replace('.e2e.test.js', '');
66
+ }
67
+
68
+ function findMatchingMockDirs(stem, commands) {
69
+ if (stem.startsWith('agent-')) {
70
+ const agentRoot = walkDirs(MOCKS_DIR, (_, entry) => /^agent_\d+$/.test(entry))[0];
71
+ if (!agentRoot) return [];
72
+ const entries = fs.readdirSync(agentRoot).filter((d) => fs.statSync(path.join(agentRoot, d)).isDirectory());
73
+
74
+ const op = stem.replace('agent-', '');
75
+ if (!op.includes('-')) {
76
+ return entries.filter((d) => d.startsWith(`${op}_`)).map((d) => path.join(agentRoot, d));
77
+ }
78
+ if (op.startsWith('web-')) return entries.filter((d) => d.startsWith(`web-${op.replace('web-', '')}`)).map((d) => path.join(agentRoot, d));
79
+ if (op.startsWith('java-')) return entries.filter((d) => d.startsWith(`java-${op.replace('java-', '')}`)).map((d) => path.join(agentRoot, d));
80
+ if (op.startsWith('gateway-')) return entries.filter((d) => d.startsWith(`gateway-${op.replace('gateway-', '')}`)).map((d) => path.join(agentRoot, d));
81
+ if (op.startsWith('ai-')) return entries.filter((d) => d.startsWith(`ai-${op.replace('ai-', '')}`)).map((d) => path.join(agentRoot, d));
82
+ return [];
83
+ }
84
+
85
+ const cmdLine = commands.join('\n');
86
+ if (cmdLine.includes('config-manager pull oauth2-agents')) {
87
+ return walkDirs(MOCKS_DIR, (_, entry) => /^oauth2-agents_\d+$/.test(entry));
88
+ }
89
+
90
+ return [];
91
+ }
92
+
93
+ function snapshotHealth(snapshotText) {
94
+ const hits = [];
95
+ for (const re of ERROR_PATTERNS) {
96
+ if (re.test(snapshotText)) hits.push(re.source);
97
+ }
98
+ return hits;
99
+ }
100
+
101
+ function parseSnapshotEntries(snapshotText) {
102
+ const entries = [];
103
+ const re = /exports\[`([^`]+)`\]\s*=\s*`([\s\S]*?)`;/g;
104
+ for (const m of snapshotText.matchAll(re)) {
105
+ entries.push({
106
+ name: m[1],
107
+ body: m[2],
108
+ });
109
+ }
110
+ return entries;
111
+ }
112
+
113
+ function findSuspiciousSnapshotTests(snapshotText) {
114
+ const suspiciousTests = [];
115
+ const entries = parseSnapshotEntries(snapshotText);
116
+ for (const entry of entries) {
117
+ const matchedPatterns = [];
118
+ for (const pattern of ERROR_PATTERNS) {
119
+ if (pattern.test(entry.body)) matchedPatterns.push(pattern.source);
120
+ }
121
+ if (matchedPatterns.length > 0) {
122
+ suspiciousTests.push({
123
+ testName: entry.name,
124
+ matchedPatterns,
125
+ });
126
+ }
127
+ }
128
+ return suspiciousTests;
129
+ }
130
+
131
+ function classifySuite({ testFile, commands, snapshotPath, mockDirs }) {
132
+ const reasons = [];
133
+ let verdict = 'GOOD';
134
+
135
+ if (!fs.existsSync(snapshotPath)) {
136
+ verdict = 'BAD';
137
+ reasons.push('missing snapshot file');
138
+ }
139
+
140
+ if (mockDirs.length === 0) {
141
+ verdict = 'BAD';
142
+ reasons.push('recording directory missing');
143
+ }
144
+
145
+ let snapshotCount = 0;
146
+ let suspicious = [];
147
+ let suspiciousTests = [];
148
+ if (fs.existsSync(snapshotPath)) {
149
+ const snapshotText = fs.readFileSync(snapshotPath, 'utf8');
150
+ snapshotCount = (snapshotText.match(/exports\[`/g) || []).length;
151
+ suspicious = snapshotHealth(snapshotText);
152
+ suspiciousTests = findSuspiciousSnapshotTests(snapshotText);
153
+ if (suspicious.length > 0) {
154
+ verdict = 'BAD';
155
+ reasons.push('snapshot contains explicit error signature');
156
+ }
157
+ }
158
+
159
+ const harCount = mockDirs.reduce((sum, dir) => sum + countHarFiles(dir), 0);
160
+ if (mockDirs.length > 0 && harCount === 0) {
161
+ verdict = 'BAD';
162
+ reasons.push('recording directory has no .har files');
163
+ }
164
+
165
+ return {
166
+ suite: testFile,
167
+ verdict,
168
+ reasons,
169
+ commands: commands.length,
170
+ snapshots: snapshotCount,
171
+ mockDirs: mockDirs.length,
172
+ harFiles: harCount,
173
+ suspicious,
174
+ suspiciousTests,
175
+ };
176
+ }
177
+
178
+ const results = [];
179
+ for (const testFile of TEST_FILES) {
180
+ const testPath = path.join(TEST_DIR, testFile);
181
+ const testText = fs.readFileSync(testPath, 'utf8');
182
+ const commands = parseCommands(testText);
183
+ const stem = getSuiteStem(testFile);
184
+ const snapshotPath = path.join(SNAP_DIR, `${testFile}.snap`);
185
+ const mockDirs = findMatchingMockDirs(stem, commands);
186
+
187
+ results.push(
188
+ classifySuite({
189
+ testFile,
190
+ commands,
191
+ snapshotPath,
192
+ mockDirs,
193
+ }),
194
+ );
195
+ }
196
+
197
+ const bad = results.filter((r) => r.verdict === 'BAD');
198
+ const good = results.length - bad.length;
199
+
200
+ console.log(`Audited suites: ${results.length}`);
201
+ console.log(`GOOD: ${good}`);
202
+ console.log(`BAD: ${bad.length}`);
203
+ console.log('');
204
+
205
+ for (const r of results) {
206
+ const prefix = r.verdict === 'GOOD' ? '[GOOD]' : '[BAD] ';
207
+ console.log(`${prefix} ${r.suite}`);
208
+ if (r.reasons.length > 0) console.log(` reasons: ${r.reasons.join('; ')}`);
209
+ if (r.suspicious.length > 0) console.log(` suspicious: ${r.suspicious.join(', ')}`);
210
+ if (r.suspiciousTests.length > 0) {
211
+ for (const t of r.suspiciousTests) {
212
+ console.log(` suspicious_test: ${t.testName}`);
213
+ console.log(` matched: ${t.matchedPatterns.join(', ')}`);
214
+ }
215
+ }
216
+ console.log(` cmds=${r.commands} snapshots=${r.snapshots} mockDirs=${r.mockDirs} harFiles=${r.harFiles}`);
217
+ }
218
+
219
+ process.exit(bad.length > 0 ? 1 : 0);
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ function parseArgs(argv) {
7
+ const args = {
8
+ output: 'docs/mcp-agentic-assessment.json',
9
+ scoreboards: [
10
+ 'docs/mcp-agentic-scoreboard.batch2.json',
11
+ 'docs/mcp-agentic-scoreboard.batch3.json',
12
+ 'docs/mcp-agentic-scoreboard.batch4.json',
13
+ ],
14
+ };
15
+
16
+ for (let i = 2; i < argv.length; i += 1) {
17
+ const token = argv[i];
18
+ if (token === '--output') {
19
+ args.output = argv[i + 1] || args.output;
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (token === '--scoreboard') {
24
+ args.scoreboards.push(argv[i + 1]);
25
+ i += 1;
26
+ }
27
+ }
28
+
29
+ return args;
30
+ }
31
+
32
+ function readJson(filePath) {
33
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
+ }
35
+
36
+ function round(value, decimals = 2) {
37
+ const scale = 10 ** decimals;
38
+ return Math.round(value * scale) / scale;
39
+ }
40
+
41
+ function normalizeMetrics(metrics) {
42
+ return {
43
+ runCount: metrics.runCount || 0,
44
+ completedCount: metrics.completedCount || 0,
45
+ completionRate: metrics.completionRate || 0,
46
+ meanAttemptsPerTask: metrics.meanAttemptsPerTask || 0,
47
+ firstAttemptSuccessRate: metrics.firstAttemptSuccessRate || 0,
48
+ duplicateRetryRate: metrics.duplicateRetryRate || 0,
49
+ intentionalFollowUpRate: metrics.intentionalFollowUpRate || 0,
50
+ temptationFailureRate:
51
+ typeof metrics.temptationFailureRate === 'number'
52
+ ? metrics.temptationFailureRate
53
+ : 0,
54
+ };
55
+ }
56
+
57
+ function computeBatchWeightedScore(metrics) {
58
+ const m = normalizeMetrics(metrics);
59
+
60
+ // Heavier penalty for temptation failures, light penalty for intentional follow-ups.
61
+ const weightedPenalty =
62
+ (100 - m.completionRate) * 1.5 +
63
+ (100 - m.firstAttemptSuccessRate) * 1.0 +
64
+ m.meanAttemptsPerTask * 12 +
65
+ m.duplicateRetryRate * 1.0 +
66
+ m.intentionalFollowUpRate * 0.15 +
67
+ m.temptationFailureRate * 1.8;
68
+
69
+ return round(1000 - weightedPenalty, 3);
70
+ }
71
+
72
+ function aggregateVariant(variantName, batchEntries) {
73
+ let runCount = 0;
74
+ let weightedCompletion = 0;
75
+ let weightedFirstAttempt = 0;
76
+ let weightedMeanAttempts = 0;
77
+ let weightedDuplicateRetries = 0;
78
+ let weightedIntentionalFollowUps = 0;
79
+ let weightedTemptationFailures = 0;
80
+ let weightedScoreTotal = 0;
81
+
82
+ const batchScores = {};
83
+
84
+ for (const entry of batchEntries) {
85
+ const metrics = normalizeMetrics(entry.metrics);
86
+ const weight = Math.max(1, metrics.runCount);
87
+ const batchScore = computeBatchWeightedScore(metrics);
88
+
89
+ batchScores[entry.batch] = {
90
+ ...metrics,
91
+ weightedScore: batchScore,
92
+ };
93
+
94
+ runCount += weight;
95
+ weightedCompletion += metrics.completionRate * weight;
96
+ weightedFirstAttempt += metrics.firstAttemptSuccessRate * weight;
97
+ weightedMeanAttempts += metrics.meanAttemptsPerTask * weight;
98
+ weightedDuplicateRetries += metrics.duplicateRetryRate * weight;
99
+ weightedIntentionalFollowUps += metrics.intentionalFollowUpRate * weight;
100
+ weightedTemptationFailures += metrics.temptationFailureRate * weight;
101
+ weightedScoreTotal += batchScore * weight;
102
+ }
103
+
104
+ const denominator = runCount || 1;
105
+
106
+ return {
107
+ variant: variantName,
108
+ batches: batchScores,
109
+ aggregate: {
110
+ weightedCompletionRate: round(weightedCompletion / denominator),
111
+ weightedFirstAttemptSuccessRate: round(weightedFirstAttempt / denominator),
112
+ weightedMeanAttemptsPerTask: round(weightedMeanAttempts / denominator),
113
+ weightedDuplicateRetryRate: round(weightedDuplicateRetries / denominator),
114
+ weightedIntentionalFollowUpRate: round(weightedIntentionalFollowUps / denominator),
115
+ weightedTemptationFailureRate: round(weightedTemptationFailures / denominator),
116
+ weightedScore: round(weightedScoreTotal / denominator, 3),
117
+ },
118
+ };
119
+ }
120
+
121
+ function buildAssessment(scoreboards) {
122
+ const byVariant = new Map();
123
+
124
+ for (const scoreboardEntry of scoreboards) {
125
+ const batch = scoreboardEntry.batch;
126
+ const variants = scoreboardEntry.scoreboard.variants || {};
127
+ for (const [variantName, metrics] of Object.entries(variants)) {
128
+ const entries = byVariant.get(variantName) || [];
129
+ entries.push({ batch, metrics });
130
+ byVariant.set(variantName, entries);
131
+ }
132
+ }
133
+
134
+ const variantAssessments = [...byVariant.entries()]
135
+ .map(([variantName, entries]) => aggregateVariant(variantName, entries))
136
+ .sort((left, right) => right.aggregate.weightedScore - left.aggregate.weightedScore);
137
+
138
+ const recommended = variantAssessments[0]?.variant || null;
139
+
140
+ return {
141
+ generatedAt: new Date().toISOString(),
142
+ batches: scoreboards.map((entry) => entry.batch),
143
+ formula: {
144
+ weightedScore:
145
+ '1000 - ((100-completionRate)*1.5 + (100-firstAttemptSuccessRate)*1.0 + meanAttemptsPerTask*12 + duplicateRetryRate*1.0 + intentionalFollowUpRate*0.15 + temptationFailureRate*1.8)',
146
+ notes: [
147
+ 'Temptation failures carry a heavier penalty than intentional follow-ups.',
148
+ 'Intentional pagination/chaining follow-ups are lightly penalized to avoid over-penalizing valid workflows.',
149
+ 'Higher weightedScore is better.',
150
+ ],
151
+ },
152
+ ranking: variantAssessments,
153
+ recommendation: {
154
+ variant: recommended,
155
+ rationale:
156
+ recommended === 'agentic'
157
+ ? 'Best weighted score with strong completion/first-attempt rates and no import/export temptation failures.'
158
+ : 'Highest weighted score under the current batch evidence.',
159
+ },
160
+ };
161
+ }
162
+
163
+ function printAssessment(assessment) {
164
+ console.log('MCP Agentic Assessment');
165
+ console.log('======================');
166
+ console.log(`Batches: ${assessment.batches.join(', ')}`);
167
+ console.log(`Recommended variant: ${assessment.recommendation.variant}`);
168
+
169
+ for (const entry of assessment.ranking) {
170
+ console.log('');
171
+ console.log(`Variant: ${entry.variant}`);
172
+ console.log(` Weighted score: ${entry.aggregate.weightedScore}`);
173
+ console.log(` Completion: ${entry.aggregate.weightedCompletionRate}%`);
174
+ console.log(` First-attempt success: ${entry.aggregate.weightedFirstAttemptSuccessRate}%`);
175
+ console.log(` Mean attempts/task: ${entry.aggregate.weightedMeanAttemptsPerTask}`);
176
+ console.log(
177
+ ` Temptation failure rate: ${entry.aggregate.weightedTemptationFailureRate}%`
178
+ );
179
+ console.log(
180
+ ` Intentional follow-up rate: ${entry.aggregate.weightedIntentionalFollowUpRate}%`
181
+ );
182
+ }
183
+ }
184
+
185
+ function main() {
186
+ const args = parseArgs(process.argv);
187
+ const scoreboards = args.scoreboards.map((scoreboardPath) => {
188
+ const resolved = path.resolve(process.cwd(), scoreboardPath);
189
+ const scoreboard = readJson(resolved);
190
+ const batchMatch = scoreboardPath.match(/batch\d+/i);
191
+ const batch = batchMatch ? batchMatch[0].toLowerCase() : path.basename(scoreboardPath);
192
+ return { batch, path: scoreboardPath, scoreboard };
193
+ });
194
+
195
+ const assessment = buildAssessment(scoreboards);
196
+ printAssessment(assessment);
197
+
198
+ const outputPath = path.resolve(process.cwd(), args.output);
199
+ fs.writeFileSync(outputPath, JSON.stringify(assessment, null, 2), 'utf8');
200
+ console.log('');
201
+ console.log(`Wrote assessment JSON to: ${outputPath}`);
202
+ }
203
+
204
+ main();