fraim-framework 2.0.124 → 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/bin/fraim.js +1 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +56 -17
- package/dist/src/cli/utils/agent-adapters.js +2 -2
- package/dist/src/cli/utils/project-bootstrap.js +4 -3
- package/dist/src/core/quality-evidence.js +81 -8
- package/dist/src/core/utils/git-utils.js +21 -4
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +3 -5
- package/dist/src/local-mcp-server/agent-token-prices.js +114 -0
- package/dist/src/local-mcp-server/codex-token-adapter.js +232 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +21 -8
- package/dist/src/local-mcp-server/stdio-server.js +29 -8
- package/dist/src/local-mcp-server/token-adapter-registry.js +64 -0
- package/dist/src/local-mcp-server/usage-collector.js +25 -0
- package/index.js +83 -83
- package/package.json +5 -1
- package/dist/src/cli/services/device-flow-service.js +0 -83
- package/dist/src/local-mcp-server/prometheus-scraper.js +0 -152
package/bin/fraim.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FRAIM_INVOCATION_BODY = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
3
|
+
exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
4
|
+
exports.buildFraimInvocationBody = buildFraimInvocationBody;
|
|
4
5
|
exports.buildClaudeSkillContent = buildClaudeSkillContent;
|
|
5
6
|
exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
|
|
6
7
|
exports.buildClaudeSlashCommandContent = buildClaudeSlashCommandContent;
|
|
@@ -14,9 +15,45 @@ exports.CURSOR_MDC_FRONTMATTER = `---
|
|
|
14
15
|
description: FRAIM discovery and execution contract
|
|
15
16
|
alwaysApply: true
|
|
16
17
|
---`;
|
|
17
|
-
exports.
|
|
18
|
+
exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
|
|
19
|
+
'fraim_connect',
|
|
20
|
+
'list_fraim_jobs',
|
|
21
|
+
'get_fraim_job',
|
|
22
|
+
'get_fraim_file',
|
|
23
|
+
'seekMentoring'
|
|
24
|
+
];
|
|
25
|
+
function buildDeferredToolBootstrapSection(profile) {
|
|
26
|
+
if (profile === 'none') {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const preloadList = exports.FRAIM_DEFERRED_TOOL_PRELOAD.map((toolName) => `\`${toolName}\``).join(', ');
|
|
30
|
+
switch (profile) {
|
|
31
|
+
case 'claude-toolsearch':
|
|
32
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
33
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
|
|
34
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
35
|
+
|
|
36
|
+
`;
|
|
37
|
+
case 'codex-tool-search':
|
|
38
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
39
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
|
|
40
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
41
|
+
|
|
42
|
+
`;
|
|
43
|
+
case 'generic-tool-discovery':
|
|
44
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
45
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
|
|
46
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
18
47
|
|
|
19
|
-
|
|
48
|
+
`;
|
|
49
|
+
default:
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function buildFraimInvocationBody(profile = 'none') {
|
|
54
|
+
return `Follow this process:
|
|
55
|
+
|
|
56
|
+
${buildDeferredToolBootstrapSection(profile)}1. **If the user did not specify a FRAIM job or topic**:
|
|
20
57
|
Call \`list_fraim_jobs()\` to discover available jobs. Present the results grouped by the categories returned by the server. For each group, list 3-5 of the most relevant jobs with a one-line description.
|
|
21
58
|
|
|
22
59
|
2. **Find the match**:
|
|
@@ -27,20 +64,22 @@ exports.FRAIM_INVOCATION_BODY = `Follow this process:
|
|
|
27
64
|
- For skills, use the content returned by \`get_fraim_file(...)\`.
|
|
28
65
|
|
|
29
66
|
4. **Execute**:
|
|
30
|
-
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
31
|
-
- For skills, apply the skill steps directly to the user's current context.
|
|
67
|
+
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
68
|
+
- For skills, apply the skill steps directly to the user's current context.
|
|
32
69
|
`;
|
|
70
|
+
}
|
|
71
|
+
exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
|
|
33
72
|
function buildClaudeSkillContent() {
|
|
34
|
-
return `# FRAIM
|
|
35
|
-
|
|
36
|
-
${
|
|
73
|
+
return `# FRAIM
|
|
74
|
+
|
|
75
|
+
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
37
76
|
}
|
|
38
77
|
function buildClaudeCommandShimContent() {
|
|
39
|
-
return `# FRAIM Compatibility Command
|
|
40
|
-
|
|
41
|
-
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
42
|
-
|
|
43
|
-
${
|
|
78
|
+
return `# FRAIM Compatibility Command
|
|
79
|
+
|
|
80
|
+
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
81
|
+
|
|
82
|
+
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
44
83
|
}
|
|
45
84
|
function buildClaudeSlashCommandContent() {
|
|
46
85
|
return buildClaudeCommandShimContent();
|
|
@@ -50,23 +89,23 @@ function buildCursorMentionRuleContent() {
|
|
|
50
89
|
|
|
51
90
|
# FRAIM
|
|
52
91
|
|
|
53
|
-
${
|
|
92
|
+
${buildFraimInvocationBody('generic-tool-discovery')}
|
|
54
93
|
`;
|
|
55
94
|
}
|
|
56
95
|
function buildCodexSkillContent() {
|
|
57
96
|
return `# FRAIM
|
|
58
97
|
|
|
59
|
-
${
|
|
98
|
+
${buildFraimInvocationBody('codex-tool-search')}`;
|
|
60
99
|
}
|
|
61
100
|
function buildWindsurfCommandContent() {
|
|
62
101
|
return `# FRAIM
|
|
63
102
|
|
|
64
|
-
${
|
|
103
|
+
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
65
104
|
}
|
|
66
105
|
function buildKiroCommandContent() {
|
|
67
106
|
return `# FRAIM
|
|
68
107
|
|
|
69
|
-
${
|
|
108
|
+
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
70
109
|
}
|
|
71
110
|
function describeInvocationSurface(ideName, invocationProfile) {
|
|
72
111
|
switch (invocationProfile) {
|
|
@@ -74,7 +74,7 @@ This repository uses FRAIM.
|
|
|
74
74
|
const cursorManagedBody = buildManagedSection(`
|
|
75
75
|
# FRAIM
|
|
76
76
|
|
|
77
|
-
${ide_invocation_surfaces_1.
|
|
77
|
+
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
|
|
78
78
|
`);
|
|
79
79
|
const copilotBody = buildManagedSection(`
|
|
80
80
|
## FRAIM
|
|
@@ -101,7 +101,7 @@ Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then
|
|
|
101
101
|
`;
|
|
102
102
|
const vscodePrompt = `# FRAIM
|
|
103
103
|
|
|
104
|
-
${ide_invocation_surfaces_1.
|
|
104
|
+
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
|
|
105
105
|
return [
|
|
106
106
|
{ path: 'AGENTS.md', content: markdownBody },
|
|
107
107
|
{ path: 'CLAUDE.md', content: markdownBody },
|
|
@@ -8,6 +8,7 @@ exports.recordPathStatus = recordPathStatus;
|
|
|
8
8
|
exports.buildInitProjectSummary = buildInitProjectSummary;
|
|
9
9
|
exports.printInitProjectSummary = printInitProjectSummary;
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const ONBOARDING_VIDEO_PLAYLIST_URL = 'https://fraimworks.ai/resources.html#videos';
|
|
11
12
|
function formatModeLabel(mode) {
|
|
12
13
|
switch (mode) {
|
|
13
14
|
case 'conversational':
|
|
@@ -21,11 +22,11 @@ function formatModeLabel(mode) {
|
|
|
21
22
|
function getModeSpecificNextStep(mode) {
|
|
22
23
|
switch (mode) {
|
|
23
24
|
case 'conversational':
|
|
24
|
-
return
|
|
25
|
+
return `The agent will focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
25
26
|
case 'split':
|
|
26
|
-
return
|
|
27
|
+
return `The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
27
28
|
default:
|
|
28
|
-
return
|
|
29
|
+
return `The agent will review the detected repo setup, then ask only for the highest-value missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
function createInitProjectResult(projectName, mode) {
|
|
@@ -26,7 +26,11 @@ exports.validateQualityEvidence = validateQualityEvidence;
|
|
|
26
26
|
exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
|
|
27
27
|
exports.QUALITY_REGISTRY = {
|
|
28
28
|
// Customer Development
|
|
29
|
-
|
|
29
|
+
// review-customer-development is the sole quality emitter for this stage.
|
|
30
|
+
// process-interview-notes is retained here for stage mapping only; it does
|
|
31
|
+
// not emit (enforced: false).
|
|
32
|
+
'process-interview-notes': { stage: 'customer-development', enforced: false },
|
|
33
|
+
'review-customer-development': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
|
|
30
34
|
'triage-customer-needs': { stage: 'customer-development', enforced: true, telemetryKind: 'gate' },
|
|
31
35
|
'interview-preparation': { stage: 'customer-discovery', enforced: false },
|
|
32
36
|
// Business Strategy
|
|
@@ -39,6 +43,7 @@ exports.QUALITY_REGISTRY = {
|
|
|
39
43
|
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
40
44
|
// Security
|
|
41
45
|
'security-review': { stage: 'security', enforced: true, telemetryKind: 'score' },
|
|
46
|
+
'production-readiness-review': { stage: 'production-readiness', enforced: true, telemetryKind: 'score' },
|
|
42
47
|
// Fundraising
|
|
43
48
|
'investor-pitch-preparation': { stage: 'fundraising', enforced: false },
|
|
44
49
|
// Go-to-Market
|
|
@@ -73,6 +78,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
73
78
|
'product-quality': 'Product Quality',
|
|
74
79
|
'test-quality': 'Test Quality',
|
|
75
80
|
'security': 'Security',
|
|
81
|
+
'production-readiness': 'Production Readiness',
|
|
76
82
|
'fundraising': 'Fundraising',
|
|
77
83
|
'go-to-market': 'Go-to-Market',
|
|
78
84
|
};
|
|
@@ -86,6 +92,7 @@ exports.ALL_STAGE_CATEGORIES = [
|
|
|
86
92
|
'product-quality',
|
|
87
93
|
'test-quality',
|
|
88
94
|
'security',
|
|
95
|
+
'production-readiness',
|
|
89
96
|
'fundraising',
|
|
90
97
|
'go-to-market',
|
|
91
98
|
];
|
|
@@ -114,20 +121,85 @@ const GATE_REQUIRED_FIELDS = [
|
|
|
114
121
|
const UNIVERSAL_REQUIRED_FIELDS = [
|
|
115
122
|
{ path: 'composite', type: 'number' },
|
|
116
123
|
];
|
|
124
|
+
const SCORE_DIMENSION_REQUIRED_FIELDS = [
|
|
125
|
+
{ suffix: 'score', type: 'number' },
|
|
126
|
+
{ suffix: 'rationale', type: 'string' }
|
|
127
|
+
];
|
|
128
|
+
const QUALITY_SCORE_DIMENSIONS = {
|
|
129
|
+
'review-customer-development': [
|
|
130
|
+
'icpCoherence',
|
|
131
|
+
'interviewCoverage',
|
|
132
|
+
'evidenceQuality',
|
|
133
|
+
'patternSaturation',
|
|
134
|
+
'signalToProduct'
|
|
135
|
+
],
|
|
136
|
+
'review-business-strategy': [
|
|
137
|
+
'marketEvidence',
|
|
138
|
+
'competitiveRigor',
|
|
139
|
+
'unitEconomics',
|
|
140
|
+
'strategicCoherence'
|
|
141
|
+
],
|
|
142
|
+
'branding-quality-audit': [
|
|
143
|
+
'clarity',
|
|
144
|
+
'differentiation',
|
|
145
|
+
'coherence',
|
|
146
|
+
'proof',
|
|
147
|
+
'identityExpressiveness',
|
|
148
|
+
'governanceReadiness'
|
|
149
|
+
],
|
|
150
|
+
'code-quality-assessment': [
|
|
151
|
+
'typeSafety',
|
|
152
|
+
'errorHandling',
|
|
153
|
+
'architecture',
|
|
154
|
+
'maintainability'
|
|
155
|
+
],
|
|
156
|
+
'test-quality-assessment': [
|
|
157
|
+
'coverage',
|
|
158
|
+
'testIntegrity',
|
|
159
|
+
'testDesign',
|
|
160
|
+
'reliability'
|
|
161
|
+
],
|
|
162
|
+
'security-review': [
|
|
163
|
+
'findingSeverity',
|
|
164
|
+
'remediationReadiness',
|
|
165
|
+
'coverageCompleteness'
|
|
166
|
+
],
|
|
167
|
+
'production-readiness-review': [
|
|
168
|
+
'securityPosture',
|
|
169
|
+
'availabilityResilience',
|
|
170
|
+
'backupRestore',
|
|
171
|
+
'observabilityOps',
|
|
172
|
+
'releaseSafety',
|
|
173
|
+
'governanceRunbooks'
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
function requiredDimensionFields(jobName) {
|
|
177
|
+
const dimensions = QUALITY_SCORE_DIMENSIONS[jobName] ?? [];
|
|
178
|
+
return dimensions.flatMap((dimension) => SCORE_DIMENSION_REQUIRED_FIELDS.map(({ suffix, type }) => ({
|
|
179
|
+
path: `${dimension}.${suffix}`,
|
|
180
|
+
type
|
|
181
|
+
})));
|
|
182
|
+
}
|
|
117
183
|
/**
|
|
118
184
|
* Returns the required quality fields for a given job.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
185
|
+
* Gate-only jobs use the gate schema. The strict V1 interview schema is
|
|
186
|
+
* pinned to process-interview-notes for defense-in-depth on the
|
|
187
|
+
* /api/analytics/quality-score endpoint. Scored quality review jobs require
|
|
188
|
+
* their stage-specific rubric dimensions as direct `{ score, rationale }`
|
|
189
|
+
* properties so the Quality view can render those dimensions dynamically.
|
|
121
190
|
*/
|
|
122
191
|
function getRequiredFieldsForJob(jobName) {
|
|
123
192
|
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
124
193
|
if (entry?.telemetryKind === 'gate') {
|
|
125
194
|
return GATE_REQUIRED_FIELDS;
|
|
126
195
|
}
|
|
127
|
-
if (
|
|
196
|
+
if (jobName === 'process-interview-notes') {
|
|
128
197
|
return CUSTOMER_DEV_REQUIRED_FIELDS;
|
|
129
198
|
}
|
|
130
|
-
return
|
|
199
|
+
return [
|
|
200
|
+
...UNIVERSAL_REQUIRED_FIELDS,
|
|
201
|
+
...requiredDimensionFields(jobName)
|
|
202
|
+
];
|
|
131
203
|
}
|
|
132
204
|
/**
|
|
133
205
|
* @deprecated Use getRequiredFieldsForJob() for stage-aware validation.
|
|
@@ -188,6 +260,9 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
188
260
|
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
189
261
|
const isGateOnly = entry?.telemetryKind === 'gate';
|
|
190
262
|
const isCustomerDev = entry?.stage === 'customer-development';
|
|
263
|
+
const dimensionExamples = (QUALITY_SCORE_DIMENSIONS[jobName] ?? ['marketEvidence', 'unitEconomics'])
|
|
264
|
+
.map((dimension) => ` ${dimension}: { score: <number>, rationale: "<string>" },`)
|
|
265
|
+
.join('\n');
|
|
191
266
|
const schemaExample = isGateOnly
|
|
192
267
|
? [
|
|
193
268
|
'```javascript',
|
|
@@ -226,9 +301,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
226
301
|
' quality: {',
|
|
227
302
|
' composite: <number 0-10>,',
|
|
228
303
|
' coaching: "<actionable recommendation>",',
|
|
229
|
-
|
|
230
|
-
' unitEconomics: { score: <number>, rationale: "<string>" },',
|
|
231
|
-
' // add other sub-scores as direct properties',
|
|
304
|
+
dimensionExamples,
|
|
232
305
|
' }',
|
|
233
306
|
'}',
|
|
234
307
|
'```'
|
|
@@ -7,6 +7,18 @@ exports.determineSchema = determineSchema;
|
|
|
7
7
|
exports.getDefaultBranch = getDefaultBranch;
|
|
8
8
|
exports.sanitizeRepoIdentifier = sanitizeRepoIdentifier;
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
|
+
function extractLocalFolderLabel(rawPath) {
|
|
11
|
+
const normalized = rawPath
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/^file:\/\//i, '')
|
|
14
|
+
.replace(/\\/g, '/')
|
|
15
|
+
.replace(/\/+$/, '');
|
|
16
|
+
if (!normalized)
|
|
17
|
+
return undefined;
|
|
18
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
19
|
+
const candidate = parts[parts.length - 1];
|
|
20
|
+
return candidate || undefined;
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
12
24
|
* Default to 15302 if not on an issue branch
|
|
@@ -106,13 +118,14 @@ function sanitizeRepoIdentifier(repoUrl) {
|
|
|
106
118
|
return undefined;
|
|
107
119
|
}
|
|
108
120
|
const trimmed = repoUrl.trim();
|
|
109
|
-
//
|
|
121
|
+
// Convert local paths to just the folder name for privacy-safe workspace labeling.
|
|
110
122
|
if (trimmed.startsWith('/') ||
|
|
111
123
|
trimmed.startsWith('\\') ||
|
|
112
|
-
/^[a-zA-Z]
|
|
124
|
+
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
113
125
|
trimmed.startsWith('./') ||
|
|
114
|
-
trimmed.startsWith('../')
|
|
115
|
-
|
|
126
|
+
trimmed.startsWith('../') ||
|
|
127
|
+
trimmed.startsWith('file://')) {
|
|
128
|
+
return extractLocalFolderLabel(trimmed);
|
|
116
129
|
}
|
|
117
130
|
// Normalize GitHub/GitLab SSH and HTTPS URLs
|
|
118
131
|
// Patterns:
|
|
@@ -144,5 +157,9 @@ function sanitizeRepoIdentifier(repoUrl) {
|
|
|
144
157
|
catch (e) {
|
|
145
158
|
// Not a valid URL
|
|
146
159
|
}
|
|
160
|
+
// Preserve plain folder labels that are already privacy-safe and not path-like.
|
|
161
|
+
if (!trimmed.includes('/') && !trimmed.includes('\\') && !trimmed.includes('://')) {
|
|
162
|
+
return trimmed;
|
|
163
|
+
}
|
|
147
164
|
return undefined;
|
|
148
165
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = void 0;
|
|
4
|
+
exports.resolveFraimJobName = resolveFraimJobName;
|
|
5
|
+
exports.isListableFraimJob = isListableFraimJob;
|
|
6
|
+
const DEPRECATED_TO_CANONICAL_JOB_MAP = {
|
|
7
|
+
'learn-and-scale': 'upskill-employee',
|
|
8
|
+
'model-behavior': 'upskill-employee',
|
|
9
|
+
'promote-learning': 'upskill-employee',
|
|
10
|
+
'refine-jobs': 'upskill-employee',
|
|
11
|
+
'refine-skills': 'upskill-employee'
|
|
12
|
+
};
|
|
13
|
+
const DIRECT_JOB_ALIASES = {
|
|
14
|
+
'sleep on learnings': 'sleep-on-learnings'
|
|
15
|
+
};
|
|
16
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = new Set(Object.keys(DEPRECATED_TO_CANONICAL_JOB_MAP));
|
|
17
|
+
function normalizeJobLookupInput(input) {
|
|
18
|
+
return input.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
19
|
+
}
|
|
20
|
+
function resolveFraimJobName(input) {
|
|
21
|
+
const normalizedJobName = normalizeJobLookupInput(input);
|
|
22
|
+
const directAliasTarget = DIRECT_JOB_ALIASES[input.trim().toLowerCase()];
|
|
23
|
+
if (directAliasTarget) {
|
|
24
|
+
return {
|
|
25
|
+
requestedJobName: input,
|
|
26
|
+
normalizedJobName,
|
|
27
|
+
canonicalJobName: directAliasTarget
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const deprecatedAliasTarget = DEPRECATED_TO_CANONICAL_JOB_MAP[normalizedJobName];
|
|
31
|
+
if (deprecatedAliasTarget) {
|
|
32
|
+
return {
|
|
33
|
+
requestedJobName: input,
|
|
34
|
+
normalizedJobName,
|
|
35
|
+
canonicalJobName: deprecatedAliasTarget,
|
|
36
|
+
deprecatedAliasTarget
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
requestedJobName: input,
|
|
41
|
+
normalizedJobName,
|
|
42
|
+
canonicalJobName: normalizedJobName
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isListableFraimJob(jobName) {
|
|
46
|
+
return !exports.DEPRECATED_FRAIM_JOB_NAMES.has(normalizeJobLookupInput(jobName));
|
|
47
|
+
}
|
|
@@ -5,15 +5,13 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
7
|
static extractMetadataBlock(content) {
|
|
8
|
-
|
|
9
|
-
const frontmatterMatch = content.match(/^[\s\S]*?---\r?\n([\s\S]+?)\r?\n---/);
|
|
8
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
10
9
|
if (frontmatterMatch) {
|
|
11
10
|
try {
|
|
12
|
-
const startIndex = frontmatterMatch.index || 0;
|
|
13
11
|
return {
|
|
14
12
|
state: 'valid',
|
|
15
13
|
metadata: JSON.parse(frontmatterMatch[1]),
|
|
16
|
-
bodyStartIndex:
|
|
14
|
+
bodyStartIndex: frontmatterMatch[0].length
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
17
|
catch {
|
|
@@ -109,7 +107,7 @@ class WorkflowParser {
|
|
|
109
107
|
overview = contentAfterMetadata;
|
|
110
108
|
}
|
|
111
109
|
const phases = new Map();
|
|
112
|
-
const phaseSections = restOfContent.split(/^##\s+Phase:\s
|
|
110
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
113
111
|
if (!metadata.phases) {
|
|
114
112
|
metadata.phases = {};
|
|
115
113
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Static price table for converting per-agent token counters to USD.
|
|
4
|
+
*
|
|
5
|
+
* Issue #330 / R1.3 / C4 (ISO 27001 A.5.31).
|
|
6
|
+
*
|
|
7
|
+
* Each entry MUST carry `source` (a citation pointing back to the vendor's
|
|
8
|
+
* published pricing page) and `verifiedOn` (an ISO date string). The
|
|
9
|
+
* `agent-token-prices.test.ts` lint fails CI if any entry exceeds 180 days
|
|
10
|
+
* since `verifiedOn` so this table is reviewed at least quarterly.
|
|
11
|
+
*
|
|
12
|
+
* Prices are USD per million tokens.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AGENT_TOKEN_PRICES = void 0;
|
|
16
|
+
exports.lookupPrice = lookupPrice;
|
|
17
|
+
exports.computeCostUsd = computeCostUsd;
|
|
18
|
+
exports.AGENT_TOKEN_PRICES = [
|
|
19
|
+
// Anthropic — Claude (used by Claude Code).
|
|
20
|
+
// Note: when Claude Code emits cost via OTLP we read it directly; this
|
|
21
|
+
// table backstops cost computation for legacy/no-cost-metric snapshots
|
|
22
|
+
// and lets per-agent comparison panels report a model-attributed cost
|
|
23
|
+
// even when a snapshot lacks a costUsd field.
|
|
24
|
+
{
|
|
25
|
+
agent: 'claude-code',
|
|
26
|
+
model: 'claude-opus-4-7',
|
|
27
|
+
inputPerMTok: 15.00,
|
|
28
|
+
outputPerMTok: 75.00,
|
|
29
|
+
cacheReadPerMTok: 1.50,
|
|
30
|
+
cacheCreationPerMTok: 18.75,
|
|
31
|
+
source: 'https://www.anthropic.com/pricing',
|
|
32
|
+
verifiedOn: '2026-04-29',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
agent: 'claude-code',
|
|
36
|
+
model: 'claude-sonnet-4-6',
|
|
37
|
+
inputPerMTok: 3.00,
|
|
38
|
+
outputPerMTok: 15.00,
|
|
39
|
+
cacheReadPerMTok: 0.30,
|
|
40
|
+
cacheCreationPerMTok: 3.75,
|
|
41
|
+
source: 'https://www.anthropic.com/pricing',
|
|
42
|
+
verifiedOn: '2026-04-29',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
agent: 'claude-code',
|
|
46
|
+
model: 'claude-haiku-4-5',
|
|
47
|
+
inputPerMTok: 1.00,
|
|
48
|
+
outputPerMTok: 5.00,
|
|
49
|
+
cacheReadPerMTok: 0.10,
|
|
50
|
+
cacheCreationPerMTok: 1.25,
|
|
51
|
+
source: 'https://www.anthropic.com/pricing',
|
|
52
|
+
verifiedOn: '2026-04-29',
|
|
53
|
+
},
|
|
54
|
+
// OpenAI — GPT-5 Codex (used by Codex CLI).
|
|
55
|
+
{
|
|
56
|
+
agent: 'codex',
|
|
57
|
+
model: 'gpt-5-codex',
|
|
58
|
+
inputPerMTok: 5.00,
|
|
59
|
+
outputPerMTok: 15.00,
|
|
60
|
+
cacheReadPerMTok: 0.50,
|
|
61
|
+
cacheCreationPerMTok: 0,
|
|
62
|
+
source: 'https://openai.com/api/pricing/',
|
|
63
|
+
verifiedOn: '2026-04-29',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
agent: 'codex',
|
|
67
|
+
model: 'gpt-5',
|
|
68
|
+
inputPerMTok: 5.00,
|
|
69
|
+
outputPerMTok: 15.00,
|
|
70
|
+
cacheReadPerMTok: 0.50,
|
|
71
|
+
cacheCreationPerMTok: 0,
|
|
72
|
+
source: 'https://openai.com/api/pricing/',
|
|
73
|
+
verifiedOn: '2026-04-29',
|
|
74
|
+
},
|
|
75
|
+
// Real Codex CLI 0.125.0 emits `gpt-5.5` on turn_context.payload.model.
|
|
76
|
+
// Treated as the gpt-5 family for pricing until OpenAI publishes a
|
|
77
|
+
// distinct rate card. Conservative estimate; refresh on next quarterly
|
|
78
|
+
// verification pass.
|
|
79
|
+
{
|
|
80
|
+
agent: 'codex',
|
|
81
|
+
model: 'gpt-5.5',
|
|
82
|
+
inputPerMTok: 5.00,
|
|
83
|
+
outputPerMTok: 15.00,
|
|
84
|
+
cacheReadPerMTok: 0.50,
|
|
85
|
+
cacheCreationPerMTok: 0,
|
|
86
|
+
source: 'https://openai.com/api/pricing/ (gpt-5 row, applied to gpt-5.5 as nearest published rate)',
|
|
87
|
+
verifiedOn: '2026-04-30',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* Look up the price entry for an agent + model. Agent is matched
|
|
92
|
+
* case-insensitively. Returns null when no matching entry exists — the
|
|
93
|
+
* adapter is then expected to skip cost computation rather than guess.
|
|
94
|
+
*/
|
|
95
|
+
function lookupPrice(agent, model) {
|
|
96
|
+
if (!agent || !model)
|
|
97
|
+
return null;
|
|
98
|
+
const a = agent.toLowerCase();
|
|
99
|
+
return exports.AGENT_TOKEN_PRICES.find(e => e.agent === a && e.model === model) || null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute USD cost from cumulative token counts. Returns 0 when no price
|
|
103
|
+
* entry exists for the agent/model — the caller decides whether to drop
|
|
104
|
+
* the snapshot entirely or surface a partial-coverage warning.
|
|
105
|
+
*/
|
|
106
|
+
function computeCostUsd(agent, model, counts) {
|
|
107
|
+
const price = lookupPrice(agent, model);
|
|
108
|
+
if (!price)
|
|
109
|
+
return 0;
|
|
110
|
+
return ((counts.inputTokens / 1_000_000) * price.inputPerMTok +
|
|
111
|
+
(counts.outputTokens / 1_000_000) * price.outputPerMTok +
|
|
112
|
+
(counts.cacheReadTokens / 1_000_000) * price.cacheReadPerMTok +
|
|
113
|
+
(counts.cacheCreationTokens / 1_000_000) * price.cacheCreationPerMTok);
|
|
114
|
+
}
|