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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework CLI Entry Point
@@ -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.FRAIM_INVOCATION_BODY = `Follow this process:
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
- 1. **If the user did not specify a FRAIM job or topic**:
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
- ${exports.FRAIM_INVOCATION_BODY}`;
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
- ${exports.FRAIM_INVOCATION_BODY}`;
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
- ${exports.FRAIM_INVOCATION_BODY}
92
+ ${buildFraimInvocationBody('generic-tool-discovery')}
54
93
  `;
55
94
  }
56
95
  function buildCodexSkillContent() {
57
96
  return `# FRAIM
58
97
 
59
- ${exports.FRAIM_INVOCATION_BODY}`;
98
+ ${buildFraimInvocationBody('codex-tool-search')}`;
60
99
  }
61
100
  function buildWindsurfCommandContent() {
62
101
  return `# FRAIM
63
102
 
64
- ${exports.FRAIM_INVOCATION_BODY}`;
103
+ ${buildFraimInvocationBody('generic-tool-discovery')}`;
65
104
  }
66
105
  function buildKiroCommandContent() {
67
106
  return `# FRAIM
68
107
 
69
- ${exports.FRAIM_INVOCATION_BODY}`;
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.FRAIM_INVOCATION_BODY}
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.FRAIM_INVOCATION_BODY}`;
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 'The agent will focus on project context, validation commands, and durable repo rules.';
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 'The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details.';
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 'The agent will review the detected repo setup, then ask only for the highest-value missing project details.';
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
- 'process-interview-notes': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
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
- * Customer-development jobs retain the strict V1 schema.
120
- * All other jobs require only a composite score (principle-based, not prescriptive).
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 (entry?.stage === 'customer-development') {
196
+ if (jobName === 'process-interview-notes') {
128
197
  return CUSTOMER_DEV_REQUIRED_FIELDS;
129
198
  }
130
- return UNIVERSAL_REQUIRED_FIELDS;
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
- ' marketEvidence: { score: <number>, rationale: "<string>" }, // flat, NO "dimensions" wrapper',
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
- // Reject local paths (Windows and POSIX)
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]:\\/.test(trimmed) ||
124
+ /^[a-zA-Z]:[\\/]/.test(trimmed) ||
113
125
  trimmed.startsWith('./') ||
114
- trimmed.startsWith('../')) {
115
- return undefined;
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
- // Allow leading comments and whitespace before frontmatter
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: startIndex + frontmatterMatch[0].length
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*/im);
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
+ }