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 +8 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -3
- package/src/imprint/audit.ts +43 -22
- package/src/imprint/claude-cli-compile.ts +43 -7
- package/src/imprint/codex-cli-compile.ts +205 -129
- package/src/imprint/inject-listener.ts +31 -3
- package/src/imprint/llm.ts +3 -3
- package/src/imprint/react-flight-limits.ts +31 -0
- package/src/imprint/record.ts +657 -46
- package/src/imprint/redact.ts +240 -30
- package/src/imprint/response-body-intent.ts +73 -0
- package/src/imprint/response-body-lifecycle.ts +32 -0
- package/src/imprint/response-body-stream.ts +704 -0
- package/src/imprint/tracing.ts +60 -238
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
|
|
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
|
+
"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,
|
|
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
|
|
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
|
|
1506
|
+
await traced(
|
|
1507
1507
|
'cli.teach',
|
|
1508
1508
|
'AGENT',
|
|
1509
1509
|
{
|
package/src/imprint/audit.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
246
|
+
});
|
|
247
|
+
recordLlmUsageSpan(
|
|
248
|
+
'compile.claude_cli_usage',
|
|
249
|
+
{
|
|
224
250
|
provider: 'claude-cli',
|
|
225
|
-
model
|
|
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
|
|
258
|
-
lastResult =
|
|
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(
|