imprint-mcp 0.6.3 → 0.6.5

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,18 @@ 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
+ When one recording yields multiple tools, all candidates are selected by default.
97
+ Imprint automatically includes upstream lookup/producer tools required by any
98
+ selected downstream tool. Use `--primary-tool` to compile only the primary tool
99
+ and its prerequisites; `--all-tools` makes the default full-set selection explicit.
100
+
101
+ The recorder also has a bounded fallback for Next.js React Server Component
102
+ responses that Chromium evicts before `Network.getResponseBody` can read them.
103
+ It streams only narrowly identified GET navigation/prefetch and POST Server
104
+ Action responses, keeps normal body capture authoritative, and writes fallback
105
+ bytes only when the normal read actually fails. The candidate policy and
106
+ resource limits are documented in the [capture protocol](docs/capture-protocol.md).
107
+
96
108
  ---
97
109
 
98
110
  ## Why Imprint?
@@ -300,7 +312,7 @@ See [Sharing Skills](docs/credential-sharing.md) for the full flow.
300
312
  | [Sharing Skills](docs/credential-sharing.md) | Credential export/import and remote provisioning |
301
313
  | [MCP Maintenance](docs/mcp-maintenance.md) | Audit, disable, restore, and prune MCP state |
302
314
  | [Troubleshooting](docs/troubleshooting.md) | Common failures and fixes |
303
- | [Tracing](docs/tracing.md) | OpenTelemetry tracing, cost rollup, and Phoenix setup |
315
+ | [Tracing](docs/tracing.md) | OpenTelemetry tracing, Phoenix-owned cost tracking, and setup |
304
316
 
305
317
  <details>
306
318
  <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.5",
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.
@@ -40,6 +40,45 @@ export function parseDuration(dur: string): number | null {
40
40
  return num;
41
41
  }
42
42
 
43
+ export type TeachSelectionMode =
44
+ | 'tool'
45
+ | 'primary'
46
+ | 'all-explicit'
47
+ | 'all-default'
48
+ | 'interactive-default-all';
49
+
50
+ /** Pure CLI selection-mode resolver shared by validation and telemetry. */
51
+ export function resolveTeachSelectionMode(opts: {
52
+ tool?: string;
53
+ primaryTool?: boolean;
54
+ allTools?: boolean;
55
+ noInteractive?: boolean;
56
+ }): { mode: TeachSelectionMode; error?: string } {
57
+ if (opts.tool && opts.allTools) {
58
+ return { mode: 'tool', error: 'error: --tool cannot be combined with --all-tools' };
59
+ }
60
+ if (opts.tool && opts.primaryTool) {
61
+ return { mode: 'tool', error: 'error: --tool cannot be combined with --primary-tool' };
62
+ }
63
+ if (opts.allTools && opts.primaryTool) {
64
+ return {
65
+ mode: 'primary',
66
+ error: 'error: --all-tools cannot be combined with --primary-tool',
67
+ };
68
+ }
69
+ return {
70
+ mode: opts.tool
71
+ ? 'tool'
72
+ : opts.primaryTool
73
+ ? 'primary'
74
+ : opts.allTools
75
+ ? 'all-explicit'
76
+ : opts.noInteractive
77
+ ? 'all-default'
78
+ : 'interactive-default-all',
79
+ };
80
+ }
81
+
43
82
  const HELP = `imprint v${VERSION} — teach an AI agent to use any website. Once.
44
83
 
45
84
  USAGE
@@ -112,7 +151,7 @@ export const VERB_HELP: Record<string, VerbHelp> = {
112
151
  summary:
113
152
  'Record a workflow, compile both artifacts, emit the tool, and connect to your AI platform — all in one interactive flow. Supports resuming incomplete runs and multiple workflows per site.',
114
153
  usage: [
115
- 'imprint teach <site> [--url <url>] [--from-session <path>] [--persist-profile] [--no-interactive] [--all-tools] [--tool <toolName>] [--provider <name>] [--model <name>] [--timeout <duration>] [--keep-test] [--skip-replay] [--from-step <step>] [--to-step <step>] [--only <step>]',
154
+ 'imprint teach <site> [--url <url>] [--from-session <path>] [--persist-profile] [--no-interactive] [--primary-tool | --all-tools] [--tool <toolName>] [--provider <name>] [--model <name>] [--timeout <duration>] [--keep-test] [--skip-replay] [--from-step <step>] [--to-step <step>] [--only <step>]',
116
155
  ],
117
156
  flags: [
118
157
  { name: '--url <url>', description: 'Starting URL (else about:blank).' },
@@ -123,13 +162,15 @@ export const VERB_HELP: Record<string, VerbHelp> = {
123
162
  { name: '--persist-profile', description: 'Reuse a stable Chrome profile for this site.' },
124
163
  {
125
164
  name: '--no-interactive',
126
- description:
127
- 'Run without prompts; compile the primary detected tool and print integration snippets.',
165
+ description: 'Run without prompts; compile all detected tools by default.',
128
166
  },
129
167
  {
130
168
  name: '--all-tools',
131
- description:
132
- 'With --no-interactive, compile every detected candidate tool instead of only the primary.',
169
+ description: 'Explicitly compile all detected tools (the default).',
170
+ },
171
+ {
172
+ name: '--primary-tool',
173
+ description: 'Compile only the primary detected tool and its upstream dependencies.',
133
174
  },
134
175
  {
135
176
  name: '--tool <toolName>',
@@ -1190,7 +1231,7 @@ async function main(argv: string[]): Promise<number> {
1190
1231
  return 2;
1191
1232
  }
1192
1233
  const outPath = values.out ?? localAuditReportPath(site);
1193
- const score = await tracedWithCostRollup(
1234
+ const score = await traced(
1194
1235
  'cli.audit',
1195
1236
  'AGENT',
1196
1237
  {
@@ -1423,6 +1464,7 @@ async function main(argv: string[]): Promise<number> {
1423
1464
  'persist-profile': { type: 'boolean' },
1424
1465
  'no-interactive': { type: 'boolean' },
1425
1466
  'all-tools': { type: 'boolean' },
1467
+ 'primary-tool': { type: 'boolean' },
1426
1468
  tool: { type: 'string' },
1427
1469
  provider: { type: 'string' },
1428
1470
  model: { type: 'string' },
@@ -1464,8 +1506,14 @@ async function main(argv: string[]): Promise<number> {
1464
1506
  return 2;
1465
1507
  }
1466
1508
  }
1467
- if (values.tool && values['all-tools']) {
1468
- console.error('error: --tool cannot be combined with --all-tools');
1509
+ const selection = resolveTeachSelectionMode({
1510
+ tool: values.tool,
1511
+ primaryTool: values['primary-tool'],
1512
+ allTools: values['all-tools'],
1513
+ noInteractive: values['no-interactive'],
1514
+ });
1515
+ if (selection.error) {
1516
+ console.error(selection.error);
1469
1517
  return 2;
1470
1518
  }
1471
1519
 
@@ -1500,10 +1548,11 @@ async function main(argv: string[]): Promise<number> {
1500
1548
  const ctrl = new AbortController();
1501
1549
  const onSigint = (): void => ctrl.abort();
1502
1550
  process.once('SIGINT', onSigint);
1551
+ const teachSelectionMode = selection.mode;
1503
1552
 
1504
1553
  try {
1505
1554
  const { teach } = await import('./imprint/teach.ts');
1506
- await tracedWithCostRollup(
1555
+ await traced(
1507
1556
  'cli.teach',
1508
1557
  'AGENT',
1509
1558
  {
@@ -1513,7 +1562,10 @@ async function main(argv: string[]): Promise<number> {
1513
1562
  'imprint.provider': values.provider ?? 'auto',
1514
1563
  'imprint.model': values.model ?? 'auto',
1515
1564
  'imprint.timeout_ms': teachTimeoutMs ?? 'default',
1516
- 'imprint.all_tools': values['all-tools'] ?? false,
1565
+ 'imprint.all_tools':
1566
+ teachSelectionMode === 'all-explicit' || teachSelectionMode === 'all-default',
1567
+ 'imprint.selection_mode': teachSelectionMode,
1568
+ 'imprint.primary_tool': values['primary-tool'] ?? false,
1517
1569
  'imprint.tool': values.tool,
1518
1570
  'imprint.no_interactive': values['no-interactive'] ?? false,
1519
1571
  'imprint.skip_replay': values['skip-replay'] ?? false,
@@ -1530,7 +1582,7 @@ async function main(argv: string[]): Promise<number> {
1530
1582
  model: values.model,
1531
1583
  maxDurationMs: teachTimeoutMs,
1532
1584
  keepTest: values['keep-test'] || process.env.IMPRINT_KEEP_TEST === '1',
1533
- allTools: values['all-tools'],
1585
+ primaryTool: values['primary-tool'],
1534
1586
  tool: values.tool,
1535
1587
  skipReplay: values['skip-replay'],
1536
1588
  fromStep: fromStepArg,
@@ -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(