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
@@ -75,6 +75,11 @@ import {
75
75
  SharedResearchExchangeSchema,
76
76
  type TeachResearchMemory,
77
77
  } from './teach-research-memory.ts';
78
+ import {
79
+ AgentInteractionSchema,
80
+ USER_INTERACTION_INSTRUCTIONS,
81
+ requestAgentInteraction,
82
+ } from './user-interaction.ts';
78
83
  export * from './master-teach-agent-contracts.ts';
79
84
  export * from './master-teach-prompt-projections.ts';
80
85
  export type { MasterTeachAnalyzer } from './native-agent-pass.ts';
@@ -1767,16 +1772,8 @@ function masterOutputSchema(input: MasterDecisionInput) {
1767
1772
  if (input.phase === 'discovery' && output.recallToolNames.length > 0)
1768
1773
  issue(ctx, ['recallToolNames'], 'initial discovery cannot recall an existing tool');
1769
1774
  const researchFollowUps = output.researchFollowUps ?? [];
1770
- if (
1771
- researchFollowUps.length > 0 &&
1772
- input.decisionPurpose !== 'research_review' &&
1773
- !input.current?.snapshot
1774
- )
1775
- issue(
1776
- ctx,
1777
- ['researchFollowUps'],
1778
- 'research follow-ups require research review or a current execution snapshot',
1779
- );
1775
+ if (researchFollowUps.length > 0 && !input.current)
1776
+ issue(ctx, ['researchFollowUps'], 'research follow-ups require an existing teaching plan');
1780
1777
  if (input.decisionPurpose === 'research_review' && output.recallToolNames.length > 0)
1781
1778
  issue(ctx, ['recallToolNames'], 'research review directs researchers, not compilers');
1782
1779
  const currentToolNames = new Set(
@@ -2179,6 +2176,7 @@ type Role =
2179
2176
  | 'focused planner'
2180
2177
  | 'API researcher'
2181
2178
  | 'master decision'
2179
+ | 'component recovery'
2182
2180
  | 'baseline MVP reviewer'
2183
2181
  | 'parameter advisor'
2184
2182
  | 'completion reviewer'
@@ -2450,6 +2448,7 @@ async function request<S extends z.ZodTypeAny>(options: {
2450
2448
  validation: unknown;
2451
2449
  schema: S;
2452
2450
  agent: MasterTeachAgentOptions;
2451
+ allowInteractions?: boolean;
2453
2452
  }): Promise<z.output<S>> {
2454
2453
  const memory = options.agent.sharedResearch;
2455
2454
  const author = `${options.agent.conversationPrefix ?? ''}${options.conversationKey}`;
@@ -2459,7 +2458,7 @@ async function request<S extends z.ZodTypeAny>(options: {
2459
2458
  input: options.input,
2460
2459
  validation: options.validation,
2461
2460
  });
2462
- const system = `${readFileSync(join(PROMPTS, options.prompt), 'utf8')}${memory ? SHARED_RESEARCH_INSTRUCTIONS : ''}\n\nWhen supplied, runTiming gives the host's current time and remaining shared run budget at this turn. Use it to choose a useful MVP and proportionate next steps, leaving time for planning, compilation, live verification, repair, and publication. These are time facts, not per-tool attempt limits or permission to weaken proof. Strategy remains yours.`;
2461
+ const system = `${readFileSync(join(PROMPTS, options.prompt), 'utf8')}${memory ? SHARED_RESEARCH_INSTRUCTIONS : ''}${USER_INTERACTION_INSTRUCTIONS}\n\nWhen supplied, runTiming gives the host's current time and remaining shared run budget at this turn. Use it to choose a useful MVP and proportionate next steps, leaving time for planning, compilation, live verification, repair, and publication. These are time facts, not per-tool attempt limits or permission to weaken proof. Strategy remains yours.`;
2463
2462
  const startedAt = Date.now();
2464
2463
  const roleExpiresAt =
2465
2464
  options.agent.timeoutMs === undefined ? undefined : startedAt + options.agent.timeoutMs;
@@ -2529,6 +2528,31 @@ async function request<S extends z.ZodTypeAny>(options: {
2529
2528
  while (true) {
2530
2529
  try {
2531
2530
  const decoded = parse(options.role, response.text, z.record(z.unknown()), attempt);
2531
+ if (decoded.interaction !== undefined) {
2532
+ if (options.allowInteractions === false)
2533
+ throw new SemanticAgentOutputError(
2534
+ options.role,
2535
+ [
2536
+ 'Return the recovery decision directly; the host dispatches the selected component owner.',
2537
+ ],
2538
+ attempt,
2539
+ );
2540
+ const interaction = parse(
2541
+ options.role,
2542
+ JSON.stringify(decoded),
2543
+ z.object({ interaction: AgentInteractionSchema }).strict(),
2544
+ attempt,
2545
+ ).interaction;
2546
+ const interactionResult = await requestAgentInteraction(interaction, signal);
2547
+ response = await analyze({
2548
+ ...(retainedCodexConversation ? {} : { originalInput: options.input }),
2549
+ validationContext: options.validation,
2550
+ interactionResult,
2551
+ instruction:
2552
+ 'Continue the same role. User input and authentication results do not prove the data operation. Cancellation or unavailable input is not approval.',
2553
+ });
2554
+ continue;
2555
+ }
2532
2556
  const { sharedResearch, ...decision } = decoded;
2533
2557
  let exchange: z.infer<typeof SharedResearchExchangeSchema> | undefined;
2534
2558
  if (sharedResearch !== undefined) {
@@ -2645,6 +2669,61 @@ export async function requestToolSelectionAdvice(
2645
2669
  agent,
2646
2670
  });
2647
2671
  }
2672
+ /** Keep exact own/dependency research in the reading path and retain other
2673
+ * operations as a small index. The master still owns every complete handoff. */
2674
+ export function focusedPlannerResearchContext(
2675
+ input: Pick<
2676
+ FocusedPlannerInput,
2677
+ | 'tool'
2678
+ | 'availableProducers'
2679
+ | 'siblingToolEvidence'
2680
+ | 'incomingChainEdges'
2681
+ | 'outgoingChainEdges'
2682
+ | 'apiResearch'
2683
+ | 'apiResearchIndex'
2684
+ >,
2685
+ memory?: TeachResearchMemory,
2686
+ ): Pick<FocusedPlannerInput, 'apiResearch' | 'apiResearchIndex'> {
2687
+ const relevantNames = new Set([
2688
+ input.tool.candidate.toolName,
2689
+ ...input.tool.candidate.dependsOnTools,
2690
+ ]);
2691
+ const namesById = new Map([
2692
+ [input.tool.id, input.tool.candidate.toolName],
2693
+ ...input.availableProducers.map(({ toolId, toolName }) => [toolId, toolName] as const),
2694
+ ...input.siblingToolEvidence.map(({ toolId, toolName }) => [toolId, toolName] as const),
2695
+ ]);
2696
+ for (const edge of [...input.incomingChainEdges, ...input.outgoingChainEdges]) {
2697
+ for (const toolId of [edge.producerToolId, edge.consumerToolId]) {
2698
+ const name = namesById.get(toolId);
2699
+ if (name) relevantNames.add(name);
2700
+ }
2701
+ }
2702
+ const index = new Map((input.apiResearchIndex ?? []).map((entry) => [entry.toolName, entry]));
2703
+ for (const handoff of input.apiResearch ?? []) {
2704
+ const observation = handoff.observation ?? handoff.observations?.at(-1);
2705
+ const handoffRef = memory?.remember(handoff) ?? index.get(handoff.toolName)?.handoffRef;
2706
+ index.set(handoff.toolName, {
2707
+ toolName: handoff.toolName,
2708
+ researchInputsSha256: handoff.researchInputsSha256,
2709
+ status: handoff.status,
2710
+ summary: handoff.summary.slice(0, 400),
2711
+ ...(observation
2712
+ ? {
2713
+ observationId: observation.id,
2714
+ executionMechanism: observation.executionMechanism,
2715
+ resultOk: observation.result.ok,
2716
+ }
2717
+ : {}),
2718
+ ...(handoffRef ? { handoffRef } : {}),
2719
+ });
2720
+ }
2721
+ return {
2722
+ apiResearch: (input.apiResearch ?? []).filter(({ toolName }) => relevantNames.has(toolName)),
2723
+ apiResearchIndex: [...index.values()],
2724
+ };
2725
+ }
2726
+
2648
2727
  export async function requestFocusedPlan(
2649
2728
  input: FocusedPlannerInput,
2650
2729
  agent: MasterTeachAgentOptions = {},
@@ -2654,7 +2733,7 @@ export async function requestFocusedPlan(
2654
2733
  role: 'focused planner',
2655
2734
  conversationKey: `tool:${checked.tool.candidate.toolName}:planner`,
2656
2735
  prompt: 'master-teach-focused-planner.md',
2657
- input: checked,
2736
+ input: { ...checked, ...focusedPlannerResearchContext(checked, agent.sharedResearch) },
2658
2737
  validation: {
2659
2738
  binding: focusedPlannerBinding(checked),
2660
2739
  recordingIndex: checked.recordingIndex,
@@ -2664,6 +2743,13 @@ export async function requestFocusedPlan(
2664
2743
  agent,
2665
2744
  });
2666
2745
  }
2746
+ // Native research passes wrap a new analyzer around the same retained child.
2747
+ // Key native delivery by family + conversation rather than that short-lived wrapper.
2748
+ const researchSiblingDelivery = new WeakMap<
2749
+ object,
2750
+ Map<string, { runId: string; handoffs: Map<string, string> }>
2751
+ >();
2752
+
2667
2753
  export async function requestApiResearchStep(
2668
2754
  input: ApiResearchInput,
2669
2755
  agent: MasterTeachAgentOptions = {},
@@ -2671,11 +2757,41 @@ export async function requestApiResearchStep(
2671
2757
  ) {
2672
2758
  const checked = ApiResearchInputValidationSchema.parse(input);
2673
2759
  const latestObservation = checked.observations.at(-1);
2760
+ const conversation = `${agent.conversationPrefix ?? ''}tool:${checked.tool.candidate.toolName}:api-researcher`;
2761
+ const retainedOwner =
2762
+ agent.provider === 'codex-cli' ? (currentNativeTeachAgents() ?? agent.analyzer) : undefined;
2763
+ const priorDelivery = retainedOwner
2764
+ ? researchSiblingDelivery.get(retainedOwner)?.get(conversation)
2765
+ : undefined;
2766
+ const previousSiblings =
2767
+ priorDelivery?.runId === checked.run.runId ? priorDelivery.handoffs : undefined;
2768
+ const memory = agent.sharedResearch;
2769
+ const followUp =
2770
+ retainedTurnDelta?.kind === 'master_follow_up' && retainedOwner && memory
2771
+ ? {
2772
+ ...retainedTurnDelta.followUp,
2773
+ siblingResearch: retainedTurnDelta.followUp.siblingResearch.filter(
2774
+ (handoff) => previousSiblings?.get(handoff.toolName) !== digest(handoff),
2775
+ ),
2776
+ retainedSiblingResearch: retainedTurnDelta.followUp.siblingResearch.flatMap((handoff) => {
2777
+ const handoffSha256 = digest(handoff);
2778
+ if (previousSiblings?.get(handoff.toolName) !== handoffSha256) return [];
2779
+ return [
2780
+ {
2781
+ toolName: handoff.toolName,
2782
+ researchInputsSha256: handoff.researchInputsSha256,
2783
+ handoffSha256,
2784
+ handoffRef: memory.remember(handoff),
2785
+ },
2786
+ ];
2787
+ }),
2788
+ }
2789
+ : undefined;
2674
2790
  const retainedInput = retainedTurnDelta
2675
2791
  ? (({ kind, ...delta }) => ({
2676
2792
  instruction:
2677
2793
  kind === 'master_follow_up'
2678
- ? 'Continue the same retained API-research conversation. Follow this new master direction using the recording evidence already in this conversation. The evidenceRef identifies the current immutable projection; if a newly selected recorded request is needed, use inspect with its exact request seq to retrieve only that evidence. Test complete candidates until the named proof gap is resolved, remains partial with a better exact handoff, or is factually blocked.'
2794
+ ? 'Continue the same retained API-research conversation. Follow this new master direction using the recording evidence already in this conversation. siblingResearch contains new or changed handoffs; retainedSiblingResearch identifies unchanged handoffs already delivered to you. Use their retained exact proof, or read handoffRef through shared research when present; a sibling test does not prove this tool. The evidenceRef identifies the current immutable projection; if a newly selected recorded request is needed, use inspect with its exact request seq to retrieve only that evidence. Test complete candidates until the named proof gap is resolved, remains partial with a better exact handoff, or is factually blocked.'
2679
2795
  : kind === 'observation'
2680
2796
  ? 'Continue the same retained API-research conversation. Evaluate this newest factual test result. Return partial only when the selected core result or a required downstream obligation remains incomplete; defer optional breadth and further minimization.'
2681
2797
  : kind === 'catalog_page'
@@ -2687,9 +2803,10 @@ export async function requestApiResearchStep(
2687
2803
  : 'Review your proposed blocker once using the retained evidence. Return another evidence-backed action when one remains, otherwise return blocked with the exact factual reason.',
2688
2804
  turnKind: kind,
2689
2805
  ...delta,
2806
+ ...(followUp ? { followUp } : {}),
2690
2807
  }))(retainedTurnDelta)
2691
2808
  : undefined;
2692
- return request({
2809
+ const output = await request({
2693
2810
  role: 'API researcher',
2694
2811
  conversationKey: `tool:${checked.tool.candidate.toolName}:api-researcher`,
2695
2812
  prompt: 'master-teach-api-researcher.md',
@@ -2717,6 +2834,19 @@ export async function requestApiResearchStep(
2717
2834
  schema: apiResearchOutputSchema(checked),
2718
2835
  agent,
2719
2836
  });
2837
+ if (
2838
+ retainedOwner &&
2839
+ checked.followUp &&
2840
+ (!retainedTurnDelta || retainedTurnDelta.kind === 'master_follow_up')
2841
+ ) {
2842
+ const conversations = researchSiblingDelivery.get(retainedOwner) ?? new Map();
2843
+ const handoffs = new Map(previousSiblings);
2844
+ for (const handoff of checked.followUp.siblingResearch)
2845
+ handoffs.set(handoff.toolName, digest(handoff));
2846
+ conversations.set(conversation, { runId: checked.run.runId, handoffs });
2847
+ researchSiblingDelivery.set(retainedOwner, conversations);
2848
+ }
2849
+ return output;
2720
2850
  }
2721
2851
  // Only retain delivery fingerprints for an actual shared analyzer conversation.
2722
2852
  // Full research history remains in the host input and the provider conversation.
@@ -2766,6 +2896,91 @@ export async function requestMasterDecision(
2766
2896
  }
2767
2897
  return output;
2768
2898
  }
2899
+ const LiveComponentRecoveryInputSchema = z.object({
2900
+ toolName: z.string().min(1),
2901
+ toolId: z.string().optional(),
2902
+ phase: z.string().default('live'),
2903
+ authenticationAvailable: z.boolean(),
2904
+ components: z.array(
2905
+ z.object({
2906
+ id: z.string().min(1),
2907
+ kind: z.string().min(1),
2908
+ label: z.string(),
2909
+ toolIds: z.array(z.string()),
2910
+ }),
2911
+ ),
2912
+ parameters: z.record(z.union([z.string(), z.number(), z.boolean()])),
2913
+ failure: z.object({
2914
+ error: z.string().min(1),
2915
+ status: z.number().optional(),
2916
+ message: z.string(),
2917
+ missing: z.unknown().optional(),
2918
+ requestStageFacts: z.unknown().optional(),
2919
+ }),
2920
+ previousRecoveries: z.number().int().nonnegative(),
2921
+ recordedAuthentication: z.object({
2922
+ credentialRequestSeqs: z.array(z.number().int().nonnegative()),
2923
+ authRequestSeqs: z.array(z.number().int().nonnegative()),
2924
+ credentialNames: z.array(z.string()),
2925
+ notes: z.string(),
2926
+ }),
2927
+ contexts: z.array(
2928
+ z.object({
2929
+ contextRef: z.string(),
2930
+ label: z.string(),
2931
+ workflowPath: z.string().optional(),
2932
+ latestAction: z.string().optional(),
2933
+ latestOk: z.boolean().optional(),
2934
+ latestStatus: z.number().optional(),
2935
+ latestError: z.string().optional(),
2936
+ }),
2937
+ ),
2938
+ });
2939
+ export type LiveComponentRecoveryInput = z.input<typeof LiveComponentRecoveryInputSchema>;
2940
+
2941
+ export async function requestLiveComponentRecovery(
2942
+ input: LiveComponentRecoveryInput,
2943
+ agent: MasterTeachAgentOptions = {},
2944
+ ) {
2945
+ const checked = LiveComponentRecoveryInputSchema.parse(input);
2946
+ return request({
2947
+ role: 'component recovery',
2948
+ conversationKey: `tool:${checked.toolName}:component-recovery`,
2949
+ prompt: 'component-recovery.md',
2950
+ input: checked,
2951
+ validation: { toolName: checked.toolName },
2952
+ schema: z
2953
+ .discriminatedUnion('action', [
2954
+ z
2955
+ .object({
2956
+ action: z.literal('authenticate'),
2957
+ plan: AgentInteractionSchema.options[1].shape.plan,
2958
+ reason: z.string().min(1),
2959
+ })
2960
+ .strict(),
2961
+ z
2962
+ .object({
2963
+ action: z.literal('recover_component'),
2964
+ componentId: z.string().min(1),
2965
+ reason: z.string().min(1),
2966
+ })
2967
+ .strict(),
2968
+ z.object({ action: z.literal('return_failure'), reason: z.string().min(1) }).strict(),
2969
+ ])
2970
+ .superRefine((decision, ctx) => {
2971
+ if (decision.action === 'authenticate' && !checked.authenticationAvailable)
2972
+ issue(ctx, ['action'], 'No authentication owner is attached');
2973
+ if (
2974
+ decision.action === 'recover_component' &&
2975
+ !checked.components.some((c) => c.id === decision.componentId)
2976
+ )
2977
+ issue(ctx, ['componentId'], 'Choose an explicitly supplied component owner');
2978
+ }),
2979
+ agent,
2980
+ allowInteractions: false,
2981
+ });
2982
+ }
2983
+
2769
2984
  export async function requestParameterSelectionAdvice(
2770
2985
  input: ParameterSelectionAdvisorInput,
2771
2986
  agent: MasterTeachAgentOptions = {},
@@ -328,7 +328,14 @@ export function invocationOutcomeCheck(input: {
328
328
  );
329
329
  } else {
330
330
  facts.push(
331
- ReceiptFactSchema.parse({ ...invocationBase, status: 'failed' }),
331
+ ReceiptFactSchema.parse({
332
+ ...invocationBase,
333
+ status: 'failed',
334
+ errorCode: input.outcome.result.error,
335
+ ...(input.outcome.result.status === undefined
336
+ ? {}
337
+ : { httpStatus: input.outcome.result.status }),
338
+ }),
332
339
  ReceiptFactSchema.parse({
333
340
  kind: 'result',
334
341
  subject: input.subject,