imprint-mcp 0.7.0 → 0.7.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.
Files changed (58) hide show
  1. package/README.md +66 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +173 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +271 -43
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +242 -60
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +147 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +284 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-cdp-sessions.ts +7 -2
  48. package/src/imprint/mcp-compile-server.ts +62 -850
  49. package/src/imprint/mcp-server.ts +667 -91
  50. package/src/imprint/mcp-session-recovery.ts +167 -0
  51. package/src/imprint/native-teach-agents.ts +232 -108
  52. package/src/imprint/runtime.ts +416 -47
  53. package/src/imprint/stealth-chromium.ts +67 -7
  54. package/src/imprint/teach-browser-resources.ts +88 -0
  55. package/src/imprint/teach-stage-progress.ts +38 -0
  56. package/src/imprint/terminal-user-prompt.ts +42 -0
  57. package/src/imprint/types.ts +51 -18
  58. package/src/imprint/user-interaction.ts +836 -0
@@ -1,5 +1,53 @@
1
1
  import { ensurePlaywrightChromiumInstalled } from './chromium.ts';
2
2
 
3
+ interface CompatCdpClient {
4
+ send(method: string, parameters?: unknown): Promise<unknown>;
5
+ }
6
+ interface CompatPage {
7
+ _client(): CompatCdpClient;
8
+ }
9
+ interface PageSetupPlugin {
10
+ onPageCreated(page: CompatPage): Promise<void>;
11
+ }
12
+
13
+ /** Keep a plugin's fire-and-forget CDP calls inside its caught lifecycle hook. */
14
+ export function awaitPluginPageSetup<T extends PageSetupPlugin>(plugin: T): T {
15
+ const original = plugin.onPageCreated.bind(plugin);
16
+ plugin.onPageCreated = async (page) => {
17
+ const commands: Promise<unknown>[] = [];
18
+ const guardedPage = new Proxy(page, {
19
+ get(target, property, receiver) {
20
+ if (property !== '_client') return Reflect.get(target, property, receiver);
21
+ return () => {
22
+ const client = target._client();
23
+ return new Proxy(client, {
24
+ get(clientTarget, clientProperty, clientReceiver) {
25
+ if (clientProperty !== 'send')
26
+ return Reflect.get(clientTarget, clientProperty, clientReceiver);
27
+ return (method: string, parameters?: unknown) => {
28
+ const command = clientTarget.send(method, parameters);
29
+ // Observe immediately, even if the hook yields before it returns.
30
+ void command.catch(() => {});
31
+ commands.push(command);
32
+ return command;
33
+ };
34
+ },
35
+ });
36
+ };
37
+ },
38
+ });
39
+ try {
40
+ await original(guardedPage);
41
+ } finally {
42
+ await Promise.all(commands);
43
+ }
44
+ };
45
+ return plugin;
46
+ }
47
+
48
+ let chromiumPromise: Promise<typeof import('playwright').chromium> | undefined;
49
+ let stealthAvailable = false;
50
+
3
51
  /**
4
52
  * Shared loader for Playwright's chromium with the stealth plugin applied.
5
53
  *
@@ -18,13 +66,30 @@ import { ensurePlaywrightChromiumInstalled } from './chromium.ts';
18
66
  * thrown error into their own result shape.
19
67
  */
20
68
  export async function getStealthChromium(): Promise<typeof import('playwright').chromium> {
69
+ chromiumPromise ??= loadStealthChromium();
70
+ return await chromiumPromise;
71
+ }
72
+
73
+ async function loadStealthChromium(): Promise<typeof import('playwright').chromium> {
21
74
  try {
22
75
  const pwExtra = await import('playwright-extra');
23
76
  const stealthMod = await import('puppeteer-extra-plugin-stealth');
24
77
  const stealthFactory =
25
78
  (stealthMod as { default?: () => unknown }).default ??
26
79
  (stealthMod as unknown as () => unknown);
80
+ // This upstream evasion does not await client.send. Playwright-extra catches
81
+ // rejected hook promises, so attach the command to that hook rather than
82
+ // allowing a browser-close race to become an unhandled rejection.
83
+ const userAgentModule = await import(
84
+ // @ts-expect-error The upstream evasion does not publish TypeScript declarations.
85
+ 'puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js'
86
+ );
87
+ const userAgentFactory = userAgentModule.default as (options: unknown) => PageSetupPlugin;
88
+ pwExtra.chromium.plugins.setDependencyResolution('stealth/evasions/user-agent-override', ((
89
+ options: unknown,
90
+ ) => awaitPluginPageSetup(userAgentFactory(options))) as never);
27
91
  pwExtra.chromium.use(stealthFactory() as never);
92
+ stealthAvailable = true;
28
93
  return pwExtra.chromium as unknown as typeof import('playwright').chromium;
29
94
  } catch {
30
95
  const pw = await import('playwright');
@@ -45,13 +110,8 @@ export async function getStealthChromium(): Promise<typeof import('playwright').
45
110
  * protection. Import resolution is cached, so probing here is cheap.
46
111
  */
47
112
  export async function isStealthPluginAvailable(): Promise<boolean> {
48
- try {
49
- await import('playwright-extra');
50
- await import('puppeteer-extra-plugin-stealth');
51
- return true;
52
- } catch {
53
- return false;
54
- }
113
+ await getStealthChromium();
114
+ return stealthAvailable;
55
115
  }
56
116
 
57
117
  /**
@@ -0,0 +1,88 @@
1
+ import type { CdpBrowserFetch } from './cdp-browser-fetch.ts';
2
+ import { abortSignalError, withAbortSignal } from './concurrency.ts';
3
+
4
+ let previousCheck: Promise<void> = Promise.resolve();
5
+ // The pass owns its pool and drains it on exit. A weak reference lets the host
6
+ // evict an idle pass without extending its lifetime or sharing its browser.
7
+ let retainedPool: WeakRef<Map<string, CdpBrowserFetch>> | undefined;
8
+ const activeBudgetPools = new WeakSet<Map<string, CdpBrowserFetch>>();
9
+
10
+ /** Evict idle contexts before a transport launches its next browser. Omitting
11
+ * the key reserves the budget for a transport that does not use the CDP pool. */
12
+ export async function prepareTeachBrowserContext(
13
+ pool: Map<string, CdpBrowserFetch> | undefined,
14
+ selectedKey?: string,
15
+ ): Promise<void> {
16
+ if (!pool || !activeBudgetPools.has(pool)) return;
17
+ const keep = selectedKey === undefined ? undefined : pool.get(selectedKey);
18
+ const older = new Set<CdpBrowserFetch>();
19
+ for (const [key, browser] of pool) {
20
+ if (browser === keep) continue;
21
+ pool.delete(key);
22
+ older.add(browser);
23
+ }
24
+ await Promise.allSettled([...older].map((browser) => browser.close()));
25
+ }
26
+
27
+ async function drain(pool: Map<string, CdpBrowserFetch>): Promise<void> {
28
+ const browsers = [...new Set(pool.values())];
29
+ pool.clear();
30
+ await Promise.allSettled(browsers.map((browser) => browser.close()));
31
+ }
32
+
33
+ async function retainLatestBrowser(pool: Map<string, CdpBrowserFetch>): Promise<void> {
34
+ const latest = [...pool.entries()].at(-1);
35
+ if (!latest) return;
36
+ const older = new Set<CdpBrowserFetch>();
37
+ for (const [key, browser] of pool) {
38
+ if (browser === latest[1]) continue;
39
+ pool.delete(key);
40
+ older.add(browser);
41
+ }
42
+ await Promise.allSettled([...older].map((browser) => browser.close()));
43
+ }
44
+
45
+ /** Optional host resource limit; agent scheduling and auth checkpoints are unchanged. */
46
+ export async function withTeachBrowserResources<T>(
47
+ callerPool: Map<string, CdpBrowserFetch> | undefined,
48
+ signal: AbortSignal | undefined,
49
+ work: (pool: Map<string, CdpBrowserFetch> | undefined) => Promise<T>,
50
+ ): Promise<T> {
51
+ if (process.env.IMPRINT_TEACH_LOW_MEMORY !== '1') return await work(callerPool);
52
+
53
+ const ahead = previousCheck;
54
+ let release = () => {};
55
+ const held = new Promise<void>((resolve) => {
56
+ release = resolve;
57
+ });
58
+ previousCheck = ahead.then(() => held);
59
+ const pool = callerPool ?? new Map<string, CdpBrowserFetch>();
60
+ let admitted = false;
61
+ let completed = false;
62
+ try {
63
+ await withAbortSignal(() => ahead, signal);
64
+ if (signal?.aborted) throw abortSignalError(signal);
65
+ admitted = true;
66
+ const previous = retainedPool?.deref();
67
+ if (previous && previous !== pool) await drain(previous);
68
+ retainedPool = undefined;
69
+ activeBudgetPools.add(pool);
70
+ const result = await work(pool);
71
+ completed = true;
72
+ return result;
73
+ } finally {
74
+ try {
75
+ if (admitted) {
76
+ activeBudgetPools.delete(pool);
77
+ if (callerPool && completed && !signal?.aborted) {
78
+ await retainLatestBrowser(pool);
79
+ retainedPool = new WeakRef(pool);
80
+ } else {
81
+ await drain(pool);
82
+ }
83
+ }
84
+ } finally {
85
+ release();
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,38 @@
1
+ /** Keep long agent stages visible without changing their execution or deadline. */
2
+ export async function withTeachStageProgress<T>(
3
+ options: {
4
+ label: string;
5
+ report?: (message: string) => void;
6
+ intervalMs?: number;
7
+ },
8
+ work: () => Promise<T>,
9
+ ): Promise<T> {
10
+ if (!options.report) return await work();
11
+ const startedAt = performance.now();
12
+ const elapsed = (): string => {
13
+ const seconds = Math.floor((performance.now() - startedAt) / 1_000);
14
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
15
+ };
16
+ const report = (message: string): void => {
17
+ try {
18
+ options.report?.(message);
19
+ } catch {
20
+ // A diagnostic callback must not interrupt work or replace its error.
21
+ }
22
+ };
23
+ report(`${options.label} started`);
24
+ const timer = setInterval(
25
+ () => report(`${options.label} pending (${elapsed()} elapsed)`),
26
+ options.intervalMs ?? 30_000,
27
+ );
28
+ timer.unref();
29
+ let completed = false;
30
+ try {
31
+ const result = await work();
32
+ completed = true;
33
+ return result;
34
+ } finally {
35
+ clearInterval(timer);
36
+ report(`${options.label} ${completed ? 'finished' : 'stopped'} (${elapsed()} elapsed)`);
37
+ }
38
+ }
@@ -0,0 +1,42 @@
1
+ import * as p from '@clack/prompts';
2
+ import { abortSignalError } from './concurrency.ts';
3
+ import { pauseLogsForPrompt } from './log.ts';
4
+ import type { OnUserPrompt } from './user-interaction.ts';
5
+
6
+ /** The parent terminal owns input; model/stdio child processes never read it. */
7
+ export const terminalUserPrompt: OnUserPrompt = async (prompt, signal) => {
8
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
9
+ return {
10
+ status: 'unavailable',
11
+ reason:
12
+ 'User input requires an interactive terminal. Run imprint teach without --no-interactive in a terminal, or provide an onPrompt callback.',
13
+ };
14
+ }
15
+ if (signal?.aborted) throw abortSignalError(signal);
16
+ const resumeLogs = pauseLogsForPrompt();
17
+ try {
18
+ const common = {
19
+ message: prompt.message,
20
+ signal,
21
+ input: process.stdin,
22
+ output: process.stderr,
23
+ };
24
+ const answer = prompt.options
25
+ ? await p.select({
26
+ ...common,
27
+ options: prompt.options.map((value) => ({ value, label: value })),
28
+ })
29
+ : prompt.sensitive
30
+ ? await p.password({
31
+ ...common,
32
+ validate: (value) => (value?.length ? undefined : 'Enter a value or cancel.'),
33
+ })
34
+ : await p.text(common);
35
+ if (signal?.aborted) throw abortSignalError(signal);
36
+ if (p.isCancel(answer))
37
+ return { status: 'cancelled', reason: 'The user cancelled the question.' };
38
+ return { status: 'answered', value: String(answer) };
39
+ } finally {
40
+ resumeLogs();
41
+ }
42
+ };
@@ -251,6 +251,9 @@ const BootstrapCaptureSchema = z.discriminatedUnion('source', [
251
251
  ]);
252
252
  export type BootstrapCapture = z.infer<typeof BootstrapCaptureSchema>;
253
253
 
254
+ /** Managed authentication can disable transport retry for an ambiguous dispatch. */
255
+ export type BrowserRequestInit = RequestInit & { allowAmbiguousRetry?: false };
256
+
254
257
  const WorkflowRequestBaseSchema = z.object({
255
258
  method: z.string(),
256
259
  /** Sequence number of the captured request that grounds this outgoing
@@ -365,6 +368,25 @@ export type NavigationNetworkResponseMatcher = NonNullable<
365
368
  NonNullable<WorkflowRequest['navigation']>['networkResponse']
366
369
  >;
367
370
 
371
+ const AuthOutcomeSchema = z.discriminatedUnion('type', [
372
+ z
373
+ .object({
374
+ type: z.literal('pause'),
375
+ next: z.string().min(1),
376
+ resume: z.literal('automatic').optional(),
377
+ evidence: z.array(z.string().min(1)).optional().default([]),
378
+ carry: z.array(z.string().min(1)).optional().default([]),
379
+ message: z.string().min(1),
380
+ })
381
+ .strict(),
382
+ z
383
+ .object({
384
+ type: z.literal('success'),
385
+ evidence: z.array(z.string().min(1)).optional().default([]),
386
+ })
387
+ .strict(),
388
+ ]);
389
+
368
390
  const AuthConfigSchema = z
369
391
  .object({
370
392
  /** Recording-grounded auth program. Action names, boundaries, evidence,
@@ -393,23 +415,13 @@ const AuthConfigSchema = z
393
415
  .strict(),
394
416
  )
395
417
  .min(1),
396
- outcome: z.discriminatedUnion('type', [
397
- z
398
- .object({
399
- type: z.literal('pause'),
400
- next: z.string().min(1),
401
- evidence: z.array(z.string().min(1)).optional().default([]),
402
- carry: z.array(z.string().min(1)).optional().default([]),
403
- message: z.string().min(1),
404
- })
405
- .strict(),
406
- z
407
- .object({
408
- type: z.literal('success'),
409
- evidence: z.array(z.string().min(1)).optional().default([]),
410
- })
411
- .strict(),
412
- ]),
418
+ /** First matching capture against this action's final step response
419
+ * selects an outcome. A match also supplies the named capture. */
420
+ branches: z
421
+ .array(z.object({ when: RequestCaptureSchema, outcome: AuthOutcomeSchema }).strict())
422
+ .optional(),
423
+ /** Fallback when no branch matches; its evidence is still required. */
424
+ outcome: AuthOutcomeSchema,
413
425
  })
414
426
  .strict(),
415
427
  ),
@@ -430,6 +442,10 @@ const AuthConfigSchema = z
430
442
  .optional();
431
443
  export type AuthConfig = z.infer<typeof AuthConfigSchema>;
432
444
 
445
+ export function authActionOutcomes(action: NonNullable<AuthConfig>['actions'][string]) {
446
+ return [...(action.branches ?? []).map((branch) => branch.outcome), action.outcome];
447
+ }
448
+
433
449
  export function persistedCaptureName(
434
450
  config: NonNullable<AuthConfig>,
435
451
  credentialName: string,
@@ -538,7 +554,7 @@ export interface StateMissingItem {
538
554
  * values, and response content. */
539
555
  export interface RequestStageFact {
540
556
  requestIndex: number;
541
- stage: 'preparation' | 'transform' | 'send';
557
+ stage: 'preparation' | 'transform' | 'send' | 'capture' | 'repeat' | 'outcome';
542
558
  outcome: 'passed' | 'failed' | 'skipped' | 'unavailable';
543
559
  /** Final body metadata at this stage. No body bytes are retained. */
544
560
  bodyPresent?: boolean;
@@ -547,6 +563,10 @@ export interface RequestStageFact {
547
563
  bodyChanged?: boolean;
548
564
  /** HTTP status proves transport completed without exposing response content. */
549
565
  httpStatus?: number;
566
+ /** One-based attempt within the declared step; no request or response values. */
567
+ attempt?: number;
568
+ stepIndex?: number;
569
+ transportInvoked?: boolean;
550
570
  }
551
571
 
552
572
  /** Discriminated union returned by every generated tool. */
@@ -582,10 +602,23 @@ export type ToolResult<T = unknown> =
582
602
  };
583
603
  /** Value-free stage receipts for requests attempted before this failure. */
584
604
  requestStageFacts?: RequestStageFact[];
605
+ /** Exact count plus bounded recent stage facts; retries remain model-authored. */
606
+ authExecution?: {
607
+ action: string;
608
+ stepIndex: number;
609
+ requestIndex: number;
610
+ attempts: number;
611
+ phase: RequestStageFact['stage'];
612
+ transportInvoked: boolean;
613
+ responseReceived: boolean;
614
+ httpStatus?: number;
615
+ lastFailure?: { error: string; message: string };
616
+ };
585
617
  /** Recording-derived state and generic action progress. Echo this object
586
618
  * unchanged on the next auth call. */
587
619
  continuation?: Record<string, unknown>;
588
620
  nextAction?: string;
621
+ resume?: 'automatic';
589
622
  };
590
623
 
591
624
  // ─── Cron config (input to `imprint cron`) ───────────────────────────────────