imprint-mcp 0.6.0 → 0.6.2
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 +2 -0
- package/package.json +4 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/auth-compile-agent.md +149 -158
- package/prompts/build-planning.md +14 -9
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/prompts/tool-candidate-detection.md +9 -29
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/auth-compile-agent.ts +215 -155
- package/src/imprint/auth-compile-tools.ts +300 -118
- package/src/imprint/auth-verifier.ts +167 -180
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +225 -177
- package/src/imprint/build-plan.ts +3 -8
- package/src/imprint/cdp-browser-fetch.ts +930 -83
- package/src/imprint/claude-cli-compile.ts +46 -20
- package/src/imprint/codex-cli-compile.ts +436 -139
- package/src/imprint/compile-agent-types.ts +15 -10
- package/src/imprint/compile-agent.ts +88 -28
- package/src/imprint/compile-tools.ts +228 -39
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/credential-store.ts +57 -44
- package/src/imprint/emit.ts +3 -1
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +22 -3
- package/src/imprint/login.ts +107 -25
- package/src/imprint/mcp-compile-server.ts +447 -114
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +116 -35
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/recording-request.ts +36 -0
- package/src/imprint/request-capture.ts +40 -17
- package/src/imprint/runtime.ts +490 -337
- package/src/imprint/teach-state.ts +36 -2
- package/src/imprint/teach.ts +302 -146
- package/src/imprint/tool-candidates.ts +8 -10
- package/src/imprint/tool-loader.ts +12 -4
- package/src/imprint/types.ts +137 -68
|
@@ -107,7 +107,7 @@ export async function probeAllBackends(
|
|
|
107
107
|
return results;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
async function probeResolvedTool(
|
|
110
|
+
export async function probeResolvedTool(
|
|
111
111
|
opts: Pick<ProbeBackendsOptions, 'site' | 'paramOverrides'>,
|
|
112
112
|
assetRoot: string,
|
|
113
113
|
tool: ResolvedTool,
|
|
@@ -230,7 +230,7 @@ async function probeResolvedTool(
|
|
|
230
230
|
imprintVersion: VERSION,
|
|
231
231
|
schemaVersion: 2,
|
|
232
232
|
workflowHash: workflowHash(tool.workflow),
|
|
233
|
-
capabilityHash:
|
|
233
|
+
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
234
234
|
preferredOrder,
|
|
235
235
|
results,
|
|
236
236
|
};
|
|
@@ -323,16 +323,85 @@ function workflowHash(workflow: ResolvedTool['workflow']): string {
|
|
|
323
323
|
.digest('hex');
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
function
|
|
326
|
+
export function workflowCapabilityHash(workflow: ResolvedTool['workflow']): string {
|
|
327
327
|
const caps = {
|
|
328
328
|
bootstrap: Boolean(workflow.bootstrap),
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
prefersCdpReplayFirst: prefersCdpReplayFirst(workflow),
|
|
330
|
+
requestModes: normalizedUnique(workflow.requests.map((request) => request.mode ?? 'fetch')),
|
|
331
|
+
captures: normalizedUnique([
|
|
332
|
+
...(workflow.bootstrap?.captures ?? []).map(
|
|
333
|
+
(capture) => `bootstrap:${capture.source}:${capture.capability}:${capture.required}`,
|
|
334
|
+
),
|
|
335
|
+
...workflow.requests.flatMap((request) =>
|
|
336
|
+
(request.captures ?? []).map(
|
|
337
|
+
(capture) => `request:${capture.source}:${capture.capability}:${capture.required}`,
|
|
338
|
+
),
|
|
339
|
+
),
|
|
340
|
+
]),
|
|
332
341
|
};
|
|
333
342
|
return createHash('sha256').update(JSON.stringify(caps)).digest('hex');
|
|
334
343
|
}
|
|
335
344
|
|
|
345
|
+
function normalizedUnique(values: string[]): string[] {
|
|
346
|
+
return [...new Set(values)].sort();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function canRebindBackendsCacheToWorkflow(
|
|
350
|
+
tool: Pick<ResolvedTool, 'workflow'>,
|
|
351
|
+
cache: BackendsCache,
|
|
352
|
+
): boolean {
|
|
353
|
+
return (
|
|
354
|
+
typeof cache.capabilityHash === 'string' &&
|
|
355
|
+
cache.capabilityHash === workflowCapabilityHash(tool.workflow)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Carry a previously proven backend preference across a compile-time workflow
|
|
360
|
+
* revision without performing network I/O. Runtime cache loading remains
|
|
361
|
+
* hash-strict; this adapter deliberately rebinds only after the verifier has
|
|
362
|
+
* decided the existing preference is still applicable. */
|
|
363
|
+
export function rebindBackendsCacheToWorkflow(
|
|
364
|
+
tool: Pick<ResolvedTool, 'workflow' | 'dir'>,
|
|
365
|
+
cache: BackendsCache,
|
|
366
|
+
outPath = pathResolve(tool.dir, 'backends.json'),
|
|
367
|
+
): BackendsCache {
|
|
368
|
+
if (!canRebindBackendsCacheToWorkflow(tool, cache)) {
|
|
369
|
+
throw new Error('backend cache capabilities changed; run a fresh probe instead of rebinding');
|
|
370
|
+
}
|
|
371
|
+
const rebound: BackendsCache = {
|
|
372
|
+
...cache,
|
|
373
|
+
workflowHash: workflowHash(tool.workflow),
|
|
374
|
+
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
375
|
+
};
|
|
376
|
+
BackendsCacheSchema.parse(rebound);
|
|
377
|
+
writeFileSync(outPath, `${JSON.stringify(rebound, null, 2)}\n`);
|
|
378
|
+
return rebound;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Rebind an already-proven cache after compile-time metadata is stamped onto
|
|
382
|
+
* workflow.json. This performs no network I/O and intentionally does nothing
|
|
383
|
+
* for missing/invalid caches. */
|
|
384
|
+
export function rebindExistingBackendsCacheToWorkflow(toolDir: string): BackendsCache | null {
|
|
385
|
+
const workflowPath = pathResolve(toolDir, 'workflow.json');
|
|
386
|
+
if (!existsSync(workflowPath)) return null;
|
|
387
|
+
|
|
388
|
+
let workflow: ResolvedTool['workflow'];
|
|
389
|
+
try {
|
|
390
|
+
workflow = WorkflowSchema.parse(JSON.parse(readFileSync(workflowPath, 'utf8')));
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const assetRoot = pathResolve(toolDir, '..', '..');
|
|
396
|
+
const status = loadBackendsCacheStatus(workflow.site, assetRoot, toolDir, {
|
|
397
|
+
warn: false,
|
|
398
|
+
toolName: workflow.toolName,
|
|
399
|
+
});
|
|
400
|
+
if (status.status !== 'ok' && status.status !== 'stale') return null;
|
|
401
|
+
if (!canRebindBackendsCacheToWorkflow({ workflow }, status.cache)) return null;
|
|
402
|
+
return rebindBackendsCacheToWorkflow({ workflow, dir: toolDir }, status.cache);
|
|
403
|
+
}
|
|
404
|
+
|
|
336
405
|
export function persistRuntimeBackendsCache(opts: {
|
|
337
406
|
tool: ResolvedTool;
|
|
338
407
|
assetRoot: string;
|
|
@@ -343,6 +412,18 @@ export function persistRuntimeBackendsCache(opts: {
|
|
|
343
412
|
warn: false,
|
|
344
413
|
toolName: opts.tool.workflow.toolName,
|
|
345
414
|
});
|
|
415
|
+
const soleAttempt = opts.attempts.length === 1 ? opts.attempts[0] : undefined;
|
|
416
|
+
if (
|
|
417
|
+
status.status === 'ok' &&
|
|
418
|
+
status.cache.preferredOrder[0] === opts.usedBackend &&
|
|
419
|
+
soleAttempt?.backend === opts.usedBackend &&
|
|
420
|
+
soleAttempt.outcome === 'ok'
|
|
421
|
+
) {
|
|
422
|
+
// A normal call through the already-proven preferred backend is not a
|
|
423
|
+
// probe or a new backend-learning event. Preserve the original probe time
|
|
424
|
+
// and its richer cold/warm/ranking diagnostics byte-for-byte on disk.
|
|
425
|
+
return status.cache;
|
|
426
|
+
}
|
|
346
427
|
const results: BackendsCache['results'] =
|
|
347
428
|
status.status === 'ok' ? { ...status.cache.results } : {};
|
|
348
429
|
|
|
@@ -408,7 +489,7 @@ export function persistRuntimeBackendsCache(opts: {
|
|
|
408
489
|
imprintVersion: VERSION,
|
|
409
490
|
schemaVersion: 2,
|
|
410
491
|
workflowHash: workflowHash(opts.tool.workflow),
|
|
411
|
-
capabilityHash:
|
|
492
|
+
capabilityHash: workflowCapabilityHash(opts.tool.workflow),
|
|
412
493
|
preferredOrder,
|
|
413
494
|
results,
|
|
414
495
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CapturedRequest, WorkflowRequest } from './types.ts';
|
|
2
|
+
|
|
3
|
+
const TEMPLATE_PLACEHOLDER = /\$\{[^}]+\}/g;
|
|
4
|
+
|
|
5
|
+
function escapeRegExp(value: string): string {
|
|
6
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Match a concrete recorded value against a workflow template without
|
|
10
|
+
* interpreting or substituting the dynamic values themselves. */
|
|
11
|
+
function templateMatchesRecorded(template: string, recorded: string): boolean {
|
|
12
|
+
if (template === recorded) return true;
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
let pattern = '^';
|
|
15
|
+
for (const match of template.matchAll(TEMPLATE_PLACEHOLDER)) {
|
|
16
|
+
pattern += escapeRegExp(template.slice(cursor, match.index));
|
|
17
|
+
pattern += '.*?';
|
|
18
|
+
cursor = (match.index ?? 0) + match[0].length;
|
|
19
|
+
}
|
|
20
|
+
if (cursor === 0) return false;
|
|
21
|
+
pattern += `${escapeRegExp(template.slice(cursor))}$`;
|
|
22
|
+
return new RegExp(pattern).test(recorded);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recordedRequestMatchesWorkflow(
|
|
26
|
+
recorded: Pick<CapturedRequest, 'method' | 'url' | 'body'>,
|
|
27
|
+
workflow: Pick<WorkflowRequest, 'method' | 'url' | 'body'>,
|
|
28
|
+
): boolean {
|
|
29
|
+
return (
|
|
30
|
+
recorded.method.toUpperCase() === workflow.method.toUpperCase() &&
|
|
31
|
+
templateMatchesRecorded(workflow.url, recorded.url) &&
|
|
32
|
+
(workflow.body === undefined
|
|
33
|
+
? recorded.body === undefined || recorded.body === ''
|
|
34
|
+
: recorded.body !== undefined && templateMatchesRecorded(workflow.body, recorded.body))
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared, format-agnostic helpers for resolving a `RequestCapture` against a
|
|
3
|
-
* response. Used by the runtime
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* implementation means a capture locator behaves identically wherever it runs.
|
|
3
|
+
* response. Used by the runtime and by `imprint login` when resolving captures
|
|
4
|
+
* named by `authConfig.persist`. Keeping one implementation means a capture
|
|
5
|
+
* locator behaves identically wherever it runs.
|
|
7
6
|
*
|
|
8
7
|
* This is a LEAF module — it must not import other `src/imprint` modules so it
|
|
9
8
|
* can be consumed from anywhere without introducing an import cycle.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
/** Lookup a JSON path inside a parsed value. Segments may be:
|
|
13
|
-
* - an object key: `
|
|
12
|
+
* - an object key: `result.identifier`
|
|
14
13
|
* - a numeric array index: `items[0]` (bracket) or `items.0` (dot)
|
|
15
|
-
* - a field-match predicate: `
|
|
14
|
+
* - a field-match predicate: `items[type=primary]`
|
|
15
|
+
* - a JSONPath predicate: `items[?(@.type=='primary')]`
|
|
16
16
|
* → the FIRST array element whose `element[field]` stringifies to the value.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* a fixed `challenges[0]` grabs the wrong one while `[type=push]` always selects
|
|
20
|
-
* the push one. A bracketed token that is neither a number nor a `key=value`
|
|
17
|
+
* Predicates keep captures stable when array ordering is non-deterministic. A
|
|
18
|
+
* bracketed token that is neither a number nor a `key=value`
|
|
21
19
|
* predicate is treated as a literal object key (so a top-level key that itself
|
|
22
20
|
* contains dots, e.g. `[customers.userInformation.accountNumber]`, is matched
|
|
23
21
|
* verbatim rather than traversed). */
|
|
@@ -39,13 +37,8 @@ export function jsonpath(root: unknown, path: string): unknown {
|
|
|
39
37
|
if (/^\d+$/.test(inner)) {
|
|
40
38
|
tokens.push({ kind: 'index', v: Number.parseInt(inner, 10) });
|
|
41
39
|
} else {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
tokens.push({
|
|
45
|
-
kind: 'pred',
|
|
46
|
-
k: inner.slice(0, eq).trim(),
|
|
47
|
-
v: inner.slice(eq + 1).trim(),
|
|
48
|
-
});
|
|
40
|
+
const predicate = parseFieldPredicate(inner);
|
|
41
|
+
if (predicate) tokens.push(predicate);
|
|
49
42
|
else tokens.push({ kind: 'key', v: inner });
|
|
50
43
|
}
|
|
51
44
|
}
|
|
@@ -73,6 +66,36 @@ export function jsonpath(root: unknown, path: string): unknown {
|
|
|
73
66
|
return cur;
|
|
74
67
|
}
|
|
75
68
|
|
|
69
|
+
export function captureValueMatches(
|
|
70
|
+
value: unknown,
|
|
71
|
+
equals?: string | number | boolean | null,
|
|
72
|
+
): boolean {
|
|
73
|
+
if (equals !== undefined) return Object.is(value, equals);
|
|
74
|
+
return value !== undefined && value !== null && value !== '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseFieldPredicate(inner: string): { kind: 'pred'; k: string; v: string } | null {
|
|
78
|
+
const standard = /^\?\(\s*@\.([A-Za-z_$][\w$-]*)\s*==\s*(.+?)\s*\)$/.exec(inner);
|
|
79
|
+
if (standard?.[1] && standard[2] !== undefined) {
|
|
80
|
+
return { kind: 'pred', k: standard[1], v: unquotePredicateValue(standard[2]) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const eq = inner.indexOf('=');
|
|
84
|
+
if (eq < 1 || inner[eq + 1] === '=') return null;
|
|
85
|
+
return {
|
|
86
|
+
kind: 'pred',
|
|
87
|
+
k: inner.slice(0, eq).trim(),
|
|
88
|
+
v: unquotePredicateValue(inner.slice(eq + 1).trim()),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function unquotePredicateValue(value: string): string {
|
|
93
|
+
const first = value[0];
|
|
94
|
+
return value.length >= 2 && (first === "'" || first === '"') && value.at(-1) === first
|
|
95
|
+
? value.slice(1, -1)
|
|
96
|
+
: value;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/** Read a response header value, honoring the capture `mode`. Multi-valued
|
|
77
100
|
* headers are comma-split. `set-cookie` is never projected (cookies are handled
|
|
78
101
|
* by the cookie jar, not as plain header captures). */
|