imprint-mcp 0.6.3 → 0.6.4

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/README.md CHANGED
@@ -93,6 +93,13 @@ regenerate/record it with `bun scripts/demo-teach.ts`.)*
93
93
  > [!TIP]
94
94
  > All three steps happen in a single `imprint teach` command. Credentials and PII are redacted automatically before anything reaches the LLM.
95
95
 
96
+ The recorder also has a bounded fallback for Next.js React Server Component
97
+ responses that Chromium evicts before `Network.getResponseBody` can read them.
98
+ It streams only narrowly identified GET navigation/prefetch and POST Server
99
+ Action responses, keeps normal body capture authoritative, and writes fallback
100
+ bytes only when the normal read actually fails. The candidate policy and
101
+ resource limits are documented in the [capture protocol](docs/capture-protocol.md).
102
+
96
103
  ---
97
104
 
98
105
  ## Why Imprint?
@@ -300,7 +307,7 @@ See [Sharing Skills](docs/credential-sharing.md) for the full flow.
300
307
  | [Sharing Skills](docs/credential-sharing.md) | Credential export/import and remote provisioning |
301
308
  | [MCP Maintenance](docs/mcp-maintenance.md) | Audit, disable, restore, and prune MCP state |
302
309
  | [Troubleshooting](docs/troubleshooting.md) | Common failures and fixes |
303
- | [Tracing](docs/tracing.md) | OpenTelemetry tracing, cost rollup, and Phoenix setup |
310
+ | [Tracing](docs/tracing.md) | OpenTelemetry tracing, Phoenix-owned cost tracking, and setup |
304
311
 
305
312
  <details>
306
313
  <summary>More docs</summary>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/cli.ts CHANGED
@@ -7,7 +7,7 @@ import { parseArgs } from 'node:util';
7
7
  import { IS_COMPILED_BINARY } from './imprint/is-compiled.ts';
8
8
  import type { ProviderName } from './imprint/llm.ts';
9
9
  import { isDebug } from './imprint/log.ts';
10
- import { shutdownTracing, tracedWithCostRollup } from './imprint/tracing.ts';
10
+ import { shutdownTracing, traced } from './imprint/tracing.ts';
11
11
  import { VERSION } from './imprint/version.ts';
12
12
 
13
13
  /** Load .env from the project root (next to src/) if present.
@@ -1190,7 +1190,7 @@ async function main(argv: string[]): Promise<number> {
1190
1190
  return 2;
1191
1191
  }
1192
1192
  const outPath = values.out ?? localAuditReportPath(site);
1193
- const score = await tracedWithCostRollup(
1193
+ const score = await traced(
1194
1194
  'cli.audit',
1195
1195
  'AGENT',
1196
1196
  {
@@ -1503,7 +1503,7 @@ async function main(argv: string[]): Promise<number> {
1503
1503
 
1504
1504
  try {
1505
1505
  const { teach } = await import('./imprint/teach.ts');
1506
- await tracedWithCostRollup(
1506
+ await traced(
1507
1507
  'cli.teach',
1508
1508
  'AGENT',
1509
1509
  {
@@ -26,7 +26,7 @@ import { createLog } from './log.ts';
26
26
  import { imprintHomeDir } from './paths.ts';
27
27
  import { type TeachState, loadTeachState } from './teach-state.ts';
28
28
  import { type ResolvedTool, discoverTools } from './tool-loader.ts';
29
- import { llmSpanAttributes, setSpanAttributes, totalPromptTokens, traced } from './tracing.ts';
29
+ import { recordLlmUsageSpan, setSpanAttributes, totalPromptTokens, traced } from './tracing.ts';
30
30
 
31
31
  const log = createLog('audit');
32
32
 
@@ -531,6 +531,20 @@ export function buildAuditMcpCommandArgs(site: string, toolNames: readonly strin
531
531
  return ['run', CLI_PATH, 'mcp-server', site, '--include-tools', toolNames.join(',')];
532
532
  }
533
533
 
534
+ /** Normalize provider usage to OpenInference's total prompt-token contract. */
535
+ export function auditPromptTokens(
536
+ provider: ProviderName,
537
+ inputTokens: number,
538
+ cacheReadTokens: number,
539
+ cacheWriteTokens: number,
540
+ ): number {
541
+ // Codex reports input_tokens as the total and cached_input_tokens as a subset.
542
+ // Anthropic reports input_tokens as the uncached delta, with cache buckets
543
+ // alongside it, so only Anthropic usage needs summing.
544
+ if (provider === 'codex-cli') return inputTokens;
545
+ return totalPromptTokens(inputTokens, cacheReadTokens, cacheWriteTokens) ?? inputTokens;
546
+ }
547
+
534
548
  export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
535
549
  return await traced(
536
550
  'audit.session',
@@ -636,6 +650,28 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
636
650
  if (auditedToolCount === 0) {
637
651
  log('all discovered tools require human-interactive authentication; auditor not spawned');
638
652
  }
653
+ recordLlmUsageSpan(
654
+ 'audit.llm_usage',
655
+ {
656
+ provider,
657
+ model,
658
+ inputTokens: auditPromptTokens(
659
+ provider,
660
+ drive.inputTokens,
661
+ drive.cacheReadInputTokens,
662
+ drive.cacheCreationInputTokens,
663
+ ),
664
+ outputTokens: drive.outputTokens,
665
+ cacheReadTokens: drive.cacheReadInputTokens,
666
+ cacheWriteTokens: drive.cacheCreationInputTokens,
667
+ },
668
+ {
669
+ 'imprint.audit.turns': drive.turns,
670
+ ...(drive.totalCostUsd != null
671
+ ? { 'imprint.audit.provider_reported_cost_usd': drive.totalCostUsd }
672
+ : {}),
673
+ },
674
+ );
639
675
 
640
676
  // Cross-reference compile-time live verification with the audit grade.
641
677
  // The downgrade rule's purpose is to surface "flying blind" runs —
@@ -695,15 +731,6 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
695
731
  }
696
732
  }
697
733
 
698
- // TOTAL prompt (uncached + cache) for the cost calc; the cache split is
699
- // passed to llmSpanAttributes separately. Always a number here
700
- // (drive.inputTokens is non-null), so the cost-suppression happens via the
701
- // `|| undefined` at the call site below.
702
- const totalInputTokens = totalPromptTokens(
703
- drive.inputTokens,
704
- drive.cacheReadInputTokens,
705
- drive.cacheCreationInputTokens,
706
- );
707
734
  setSpanAttributes(span, {
708
735
  'imprint.audit.score': score.score,
709
736
  'imprint.audit.correct': score.correct,
@@ -727,16 +754,6 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
727
754
  'imprint.audit.timed_out': drive.timedOut,
728
755
  'imprint.audit.turns': drive.turns,
729
756
  ...(drive.totalCostUsd != null ? { 'imprint.audit.cost_usd': drive.totalCostUsd } : {}),
730
- ...llmSpanAttributes({
731
- provider,
732
- model,
733
- // `|| undefined`: when no usage was captured (e.g. spawn failure → 0
734
- // tokens), suppress a bogus $0 cost instead of emitting it.
735
- inputTokens: totalInputTokens || undefined,
736
- outputTokens: drive.outputTokens || undefined,
737
- cacheReadTokens: drive.cacheReadInputTokens || undefined,
738
- cacheWriteTokens: drive.cacheCreationInputTokens || undefined,
739
- }),
740
757
  });
741
758
 
742
759
  // Persist the full result (deterministic score + the raw model report).
@@ -1093,7 +1110,7 @@ ${buildAuditInitialPrompt(opts, unverifiedNote)}`;
1093
1110
  transcript: session.transcript,
1094
1111
  inputTokens: session.inputTokens,
1095
1112
  outputTokens: session.outputTokens,
1096
- cacheReadInputTokens: 0,
1113
+ cacheReadInputTokens: session.cacheReadInputTokens,
1097
1114
  cacheCreationInputTokens: 0,
1098
1115
  totalCostUsd: null,
1099
1116
  };
@@ -1308,6 +1325,7 @@ async function collectCodexAssistantText(
1308
1325
  let turns = 0;
1309
1326
  let inputTokens = 0;
1310
1327
  let outputTokens = 0;
1328
+ let cacheReadInputTokens = 0;
1311
1329
  const t0 = Date.now();
1312
1330
  const elapsedStr = (): string => {
1313
1331
  const s = Math.floor((Date.now() - t0) / 1000);
@@ -1357,6 +1375,7 @@ async function collectCodexAssistantText(
1357
1375
  if (evt.type === 'turn.completed') {
1358
1376
  inputTokens += evt.usage?.input_tokens ?? 0;
1359
1377
  outputTokens += evt.usage?.output_tokens ?? 0;
1378
+ cacheReadInputTokens += evt.usage?.cached_input_tokens ?? 0;
1360
1379
  log(`[${elapsedStr()}] turn ${turns} completed`);
1361
1380
  continue;
1362
1381
  }
@@ -1411,7 +1430,7 @@ async function collectCodexAssistantText(
1411
1430
  turns,
1412
1431
  inputTokens,
1413
1432
  outputTokens,
1414
- cacheReadInputTokens: 0,
1433
+ cacheReadInputTokens,
1415
1434
  cacheCreationInputTokens: 0,
1416
1435
  totalCostUsd: null,
1417
1436
  };
@@ -1461,6 +1480,8 @@ interface CodexStreamJsonEvent {
1461
1480
  usage?: {
1462
1481
  input_tokens?: number;
1463
1482
  output_tokens?: number;
1483
+ cached_input_tokens?: number;
1484
+ reasoning_output_tokens?: number;
1464
1485
  };
1465
1486
  message?: string;
1466
1487
  error?: { message?: string };
@@ -43,7 +43,7 @@ import { COMPILE_SENTINELS, compileDeadlineAfterVerification } from './mcp-compi
43
43
  import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
44
44
  import {
45
45
  endTraceSpan,
46
- llmSpanAttributes,
46
+ recordLlmUsageSpan,
47
47
  setSpanAttributes,
48
48
  startTraceSpan,
49
49
  totalPromptTokens,
@@ -88,6 +88,28 @@ const USAGE_POLICY_REFUSAL =
88
88
  /** Total attempts (1 initial + retries) when a usage-policy refusal is hit. */
89
89
  const MAX_USAGE_POLICY_ATTEMPTS = 3;
90
90
 
91
+ interface CompileUsageTotals {
92
+ turns: number;
93
+ inputTokens: number;
94
+ outputTokens: number;
95
+ cacheReadInputTokens: number;
96
+ cacheCreationInputTokens: number;
97
+ }
98
+
99
+ /** Add one paid CLI attempt to the aggregate usage reported for the compile. */
100
+ export function addCompileUsageTotals(
101
+ totals: CompileUsageTotals,
102
+ attempt: CompileUsageTotals,
103
+ ): CompileUsageTotals {
104
+ return {
105
+ turns: totals.turns + attempt.turns,
106
+ inputTokens: totals.inputTokens + attempt.inputTokens,
107
+ outputTokens: totals.outputTokens + attempt.outputTokens,
108
+ cacheReadInputTokens: totals.cacheReadInputTokens + attempt.cacheReadInputTokens,
109
+ cacheCreationInputTokens: totals.cacheCreationInputTokens + attempt.cacheCreationInputTokens,
110
+ };
111
+ }
112
+
91
113
  /** Exponential backoff with jitter between refusal retries. Spacing matters:
92
114
  * bursts of near-identical requests raise the safety-filter trip rate. */
93
115
  function usagePolicyBackoffMs(attempt: number): number {
@@ -212,6 +234,7 @@ export async function compileViaClaudeCli(
212
234
  },
213
235
  async (span) => {
214
236
  const result = await compileViaClaudeCliImpl(opts);
237
+ const model = opts.model ?? preferredAgentModel('claude-cli');
215
238
  setSpanAttributes(span, {
216
239
  'imprint.compile.outcome': result.outcome,
217
240
  'imprint.compile.turns': result.turns,
@@ -220,9 +243,12 @@ export async function compileViaClaudeCli(
220
243
  'imprint.compile.output_tokens': result.outputTokens,
221
244
  'imprint.compile.cache_read_input_tokens': result.cacheReadInputTokens,
222
245
  'imprint.compile.cache_creation_input_tokens': result.cacheCreationInputTokens,
223
- ...llmSpanAttributes({
246
+ });
247
+ recordLlmUsageSpan(
248
+ 'compile.claude_cli_usage',
249
+ {
224
250
  provider: 'claude-cli',
225
- model: opts.model ?? preferredAgentModel('claude-cli'),
251
+ model,
226
252
  // TOTAL prompt (uncached + cache); the cache split is passed separately
227
253
  // for cost. `result.inputTokens` alone is the uncached delta (often a
228
254
  // few hundred), which would mislabel `llm.token_count.prompt`.
@@ -234,8 +260,9 @@ export async function compileViaClaudeCli(
234
260
  outputTokens: result.outputTokens,
235
261
  cacheReadTokens: result.cacheReadInputTokens,
236
262
  cacheWriteTokens: result.cacheCreationInputTokens,
237
- }),
238
- });
263
+ },
264
+ { 'imprint.compile.turns': result.turns },
265
+ );
239
266
  return result;
240
267
  },
241
268
  );
@@ -251,11 +278,20 @@ async function compileViaClaudeCliImpl(
251
278
  opts: CompileViaClaudeCliOptions,
252
279
  ): Promise<CompileAgentResult> {
253
280
  let lastResult: CompileAgentResult | undefined;
281
+ let usageTotals: CompileUsageTotals = {
282
+ turns: 0,
283
+ inputTokens: 0,
284
+ outputTokens: 0,
285
+ cacheReadInputTokens: 0,
286
+ cacheCreationInputTokens: 0,
287
+ };
254
288
  for (let attempt = 1; attempt <= MAX_USAGE_POLICY_ATTEMPTS; attempt++) {
255
289
  const result = await runClaudeCliAttempt(opts);
290
+ usageTotals = addCompileUsageTotals(usageTotals, result);
291
+ const resultWithAggregateUsage = { ...result, ...usageTotals };
256
292
  const isRefusal = !result.success && USAGE_POLICY_REFUSAL.test(result.message ?? '');
257
- if (!isRefusal) return result;
258
- lastResult = result;
293
+ if (!isRefusal) return resultWithAggregateUsage;
294
+ lastResult = resultWithAggregateUsage;
259
295
  if (attempt < MAX_USAGE_POLICY_ATTEMPTS) {
260
296
  const backoffMs = usagePolicyBackoffMs(attempt);
261
297
  log(