imprint-mcp 0.6.2 → 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.
@@ -5,13 +5,20 @@
5
5
  * `[IMPRINT]` is exact-match — keep stable.
6
6
  */
7
7
 
8
- export const INJECTED_LISTENER_SOURCE = `
8
+ export function createInjectedListenerSource(eventToken: string): string {
9
+ return `
9
10
  (function imprintInjector() {
10
11
  if (window.__imprint_injected__) return;
11
12
  window.__imprint_injected__ = true;
12
13
 
13
14
  const SENTINEL = '[IMPRINT]';
15
+ const EVENT_TOKEN = ${JSON.stringify(eventToken)};
16
+ const emitLog = console.log.bind(console);
17
+ const stringify = JSON.stringify.bind(JSON);
14
18
  const MAX_VAL = 200;
19
+ let lastIntentTarget = null;
20
+ let lastIntentAt = 0;
21
+ let lastIntentKind = null;
15
22
 
16
23
  function safeStr(v) {
17
24
  try {
@@ -53,6 +60,7 @@ export const INJECTED_LISTENER_SOURCE = `
53
60
  text: safeStr((el.textContent || '').trim()),
54
61
  ariaLabel: el.getAttribute && el.getAttribute('aria-label') || null,
55
62
  href: el.tagName === 'A' ? el.getAttribute('href') : null,
63
+ absoluteHref: el.tagName === 'A' ? el.href : null,
56
64
  selector: selectorFor(el),
57
65
  };
58
66
  } catch (e) { return {}; }
@@ -60,17 +68,33 @@ export const INJECTED_LISTENER_SOURCE = `
60
68
 
61
69
  function emit(type, payload) {
62
70
  try {
63
- console.log(SENTINEL, type, JSON.stringify(payload));
71
+ emitLog(SENTINEL, EVENT_TOKEN, type, stringify(payload));
64
72
  } catch (e) { /* ignore */ }
65
73
  }
66
74
 
67
75
  function onClick(ev) {
68
76
  try {
69
- const tgt = ev.target;
77
+ onIntent(ev, 'click');
78
+ const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') || ev.target : ev.target;
70
79
  emit('click', describe(tgt));
71
80
  } catch (e) { /* ignore */ }
72
81
  }
73
82
 
83
+ function onIntent(ev, intentKind) {
84
+ try {
85
+ if (!ev.isTrusted) return;
86
+ const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') : ev.target;
87
+ if (!tgt) return;
88
+ const now = Date.now();
89
+ const activation = intentKind === 'pointerdown' || intentKind === 'click';
90
+ if (!activation && tgt === lastIntentTarget && intentKind === lastIntentKind && now - lastIntentAt < 250) return;
91
+ lastIntentTarget = tgt;
92
+ lastIntentAt = now;
93
+ lastIntentKind = intentKind;
94
+ emit('intent', { ...describe(tgt), intentAt: now, intentKind: intentKind });
95
+ } catch (e) { /* ignore */ }
96
+ }
97
+
74
98
  function onInput(ev) {
75
99
  try {
76
100
  const tgt = ev.target;
@@ -128,10 +152,14 @@ export const INJECTED_LISTENER_SOURCE = `
128
152
  // Capture phase = true so we see the event before the site has a chance to
129
153
  // stopPropagation it.
130
154
  document.addEventListener('click', onClick, true);
155
+ document.addEventListener('pointerover', (ev) => onIntent(ev, 'pointerover'), true);
156
+ document.addEventListener('pointerdown', (ev) => onIntent(ev, 'pointerdown'), true);
157
+ document.addEventListener('focusin', (ev) => onIntent(ev, 'focusin'), true);
131
158
  document.addEventListener('input', onInput, true);
132
159
  document.addEventListener('change', onChange, true);
133
160
  document.addEventListener('submit', onSubmit, true);
134
161
  })();
135
162
  `;
163
+ }
136
164
 
137
165
  export const IMPRINT_SENTINEL = '[IMPRINT]';
@@ -32,6 +32,7 @@ import {
32
32
  type probeResolvedTool,
33
33
  rebindBackendsCacheToWorkflow,
34
34
  } from './probe-backends.ts';
35
+ import { ensureImprintRuntimeLink } from './runtime-link.ts';
35
36
  import { loadCredentialStore } from './runtime.ts';
36
37
  import { isSensitiveKey } from './sensitive-keys.ts';
37
38
  import { buildZodValidator } from './tool-loader.ts';
@@ -43,9 +44,18 @@ const SYSTEM_PROMPT_PATH = pathJoin(REPO_ROOT, 'prompts', 'live-verifier-agent.m
43
44
  const MCP_SERVER_NAME = 'imprint-live-verifier';
44
45
  export const LIVE_VERIFICATION_EVIDENCE_FILE = '.live-verification-evidence.json';
45
46
  const LIVE_VERIFIER_LOG_FILE = '.live-verifier-log.jsonl';
46
- const BACKEND_PREPARATION_BUDGET_MS = 2 * 60_000;
47
+ // Cheap probes normally finish well inside the original two-minute window and
48
+ // skip browser transports. The larger ceiling is for the fallback path: a cold
49
+ // CDP request must finish before fetch-bootstrap gets its own browser attempt.
50
+ // Comprehensive preparation is sequential on purpose: concurrent browser
51
+ // transports can trip shared anti-bot/session defenses. Reserve enough time
52
+ // for the normal fetch/stealth/playbook candidates plus CDP and fetch-bootstrap
53
+ // fallbacks; the former 240s cap could kill CDP after a 150s playbook attempt.
54
+ const BACKEND_PREPARATION_BUDGET_MS = 8 * 60_000;
47
55
  const MAX_CREDITED_BACKEND_PREPARATIONS = 2;
48
56
  const BACKEND_PROBE_SHUTDOWN_GRACE_MS = 5_000;
57
+ const MAX_PROMPT_EVIDENCE_RECORDS = 48;
58
+ const MAX_PROMPT_EVIDENCE_RECORD_CHARS = 8_000;
49
59
  let evidenceTempSequence = 0;
50
60
 
51
61
  const BaselineSchema = z
@@ -158,6 +168,17 @@ export const LiveVerificationReportSchema = z
158
168
 
159
169
  type LiveVerificationReport = z.infer<typeof LiveVerificationReportSchema>;
160
170
 
171
+ export function isInfrastructureOnlyInconclusiveReport(report: LiveVerificationReport): boolean {
172
+ return (
173
+ report.status === 'inconclusive' &&
174
+ report.baseline.verdict === 'infrastructure' &&
175
+ report.issues.length === 0 &&
176
+ report.parameters.every(
177
+ (parameter) => parameter.verdict === 'works' || parameter.verdict === 'untestable',
178
+ )
179
+ );
180
+ }
181
+
161
182
  interface LiveIntegrationSuiteResult {
162
183
  exitCode: number;
163
184
  timedOut: boolean;
@@ -251,6 +272,11 @@ export async function runLiveIntegrationSuite(opts: {
251
272
  reason?: string;
252
273
  sessionLabel?: string;
253
274
  }): Promise<LiveIntegrationSuiteResult> {
275
+ // The suite is launched directly from the generated tool directory, bypassing
276
+ // normal tool discovery. Repair a stale global/worktree runtime link here so
277
+ // imports such as `imprint/compile-verification` resolve against this running
278
+ // Imprint installation.
279
+ ensureImprintRuntimeLink(dirname(dirname(opts.toolDir)));
254
280
  const integrationPath = pathJoin(opts.toolDir, 'integration.test.ts');
255
281
  if (!existsSync(integrationPath)) throw new Error('integration.test.ts is missing');
256
282
  const workflowPath = pathJoin(opts.toolDir, 'workflow.json');
@@ -461,7 +487,11 @@ export function readPersistedLiveVerificationEvidence(
461
487
 
462
488
  export function hasSuiteReceiptForSession(path: string, sessionLabel: string): boolean {
463
489
  return readPersistedLiveVerificationEvidence(path).some(
464
- (item) => item.kind === 'suite' && item.verifierSession === sessionLabel,
490
+ (item) =>
491
+ item.kind === 'suite' &&
492
+ item.verifierSession === sessionLabel &&
493
+ item.status !== 'running' &&
494
+ typeof item.finishedAt === 'string',
465
495
  );
466
496
  }
467
497
 
@@ -628,6 +658,11 @@ async function runBackendProbeSubprocess(opts: {
628
658
  attempt?: number;
629
659
  label: string;
630
660
  }): Promise<Awaited<ReturnType<typeof probeResolvedTool>>> {
661
+ const previousCacheContents = existsSync(opts.outPath)
662
+ ? readFileSync(opts.outPath, 'utf8')
663
+ : undefined;
664
+ const tool = resolveWorkflowTool(opts.workflowPath);
665
+ const assetRoot = pathJoin(tool.dir, '..', '..');
631
666
  const child = spawn(
632
667
  process.execPath,
633
668
  [
@@ -674,6 +709,30 @@ async function runBackendProbeSubprocess(opts: {
674
709
  unregisterParentCleanup();
675
710
  }
676
711
  if (outcome.timedOut) {
712
+ // probeResolvedTool checkpoints every successful candidate. If a later
713
+ // optional rung consumed the aggregate deadline, use the new valid cache
714
+ // instead of discarding an already-proven backend. Do not reuse an unchanged
715
+ // pre-probe cache: forceReprobe may have been requested because it failed.
716
+ const currentCacheContents = existsSync(opts.outPath)
717
+ ? readFileSync(opts.outPath, 'utf8')
718
+ : undefined;
719
+ const cacheStatus = loadBackendsCacheStatus(tool.site, assetRoot, tool.dir, {
720
+ warn: false,
721
+ toolName: tool.workflow.toolName,
722
+ });
723
+ if (
724
+ currentCacheContents !== undefined &&
725
+ currentCacheContents !== previousCacheContents &&
726
+ cacheStatus.status === 'ok'
727
+ ) {
728
+ appendLiveVerifierLog(opts.logPath, {
729
+ type: 'backend.probe.partial-cache-recovered',
730
+ attempt: opts.attempt,
731
+ label: opts.label,
732
+ preferredOrder: cacheStatus.cache.preferredOrder,
733
+ });
734
+ return { cache: cacheStatus.cache, outPath: opts.outPath };
735
+ }
677
736
  throw new Error(
678
737
  `backend probe exceeded its separate ${Math.round(BACKEND_PREPARATION_BUDGET_MS / 1_000)}s budget and was terminated`,
679
738
  );
@@ -684,8 +743,6 @@ async function runBackendProbeSubprocess(opts: {
684
743
  );
685
744
  }
686
745
 
687
- const tool = resolveWorkflowTool(opts.workflowPath);
688
- const assetRoot = pathJoin(tool.dir, '..', '..');
689
746
  const cacheStatus = loadBackendsCacheStatus(tool.site, assetRoot, tool.dir, {
690
747
  warn: false,
691
748
  toolName: tool.workflow.toolName,
@@ -701,6 +758,10 @@ interface LiveSemanticVerificationResult {
701
758
  provider: ProviderName;
702
759
  model: string;
703
760
  attempts: number;
761
+ /** True only when an independent verifier returned a schema-valid report.
762
+ * Provider startup/input failures synthesize an inconclusive report for
763
+ * compiler feedback but do not consume a semantic-review cycle. */
764
+ completedReview: boolean;
704
765
  }
705
766
 
706
767
  export async function runLiveSemanticVerification(opts: {
@@ -828,6 +889,7 @@ export async function runLiveSemanticVerification(opts: {
828
889
  provider: opts.provider,
829
890
  model,
830
891
  attempts: attempt,
892
+ completedReview: true,
831
893
  };
832
894
  } catch (error) {
833
895
  const message = error instanceof Error ? error.message : String(error);
@@ -890,7 +952,13 @@ export async function runLiveSemanticVerification(opts: {
890
952
  status: 'inconclusive',
891
953
  attempts: 2,
892
954
  });
893
- return { report: inconclusive, provider: opts.provider, model, attempts: 2 };
955
+ return {
956
+ report: inconclusive,
957
+ provider: opts.provider,
958
+ model,
959
+ attempts: 2,
960
+ completedReview: false,
961
+ };
894
962
  }
895
963
 
896
964
  function mergePersistedRecords(
@@ -955,13 +1023,106 @@ function buildInitialMessage(
955
1023
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
956
1024
  },
957
1025
  artifactContext: buildVerifierArtifactContext(toolDir, workflow.toolName),
958
- baselineEvidence: evidence,
1026
+ baselineEvidence: compactVerifierEvidenceContext(evidence),
959
1027
  },
960
1028
  null,
961
1029
  2,
962
1030
  )}`;
963
1031
  }
964
1032
 
1033
+ /** Keep the verifier prompt bounded without weakening the durable evidence
1034
+ * artifact. Full, sanitized receipts remain on disk; the model gets recent
1035
+ * records with representative payload previews and navigation metadata. */
1036
+ export function compactVerifierEvidenceContext(
1037
+ evidence: Array<LiveIntegrationEvidence | PersistedLiveVerificationRecord>,
1038
+ ): unknown[] {
1039
+ const omittedRecords = Math.max(0, evidence.length - MAX_PROMPT_EVIDENCE_RECORDS);
1040
+ const selected = evidence.slice(-MAX_PROMPT_EVIDENCE_RECORDS);
1041
+ const compacted = selected.map((record) => compactVerifierEvidenceRecord(record));
1042
+ return omittedRecords > 0
1043
+ ? [
1044
+ {
1045
+ kind: 'prompt-compaction',
1046
+ omittedRecords,
1047
+ note: `Full receipts remain in ${LIVE_VERIFICATION_EVIDENCE_FILE}.`,
1048
+ },
1049
+ ...compacted,
1050
+ ]
1051
+ : compacted;
1052
+ }
1053
+
1054
+ function compactVerifierEvidenceRecord(
1055
+ record: LiveIntegrationEvidence | PersistedLiveVerificationRecord,
1056
+ ): unknown {
1057
+ const compacted = compactVerifierValue(record, 0);
1058
+ const serialized = JSON.stringify(compacted);
1059
+ if (serialized.length <= MAX_PROMPT_EVIDENCE_RECORD_CHARS) return compacted;
1060
+
1061
+ const navigationKeys = [
1062
+ 'schemaVersion',
1063
+ 'kind',
1064
+ 'label',
1065
+ 'caseName',
1066
+ 'toolName',
1067
+ 'status',
1068
+ 'verifierSession',
1069
+ 'startedAt',
1070
+ 'finishedAt',
1071
+ 'requestedParams',
1072
+ 'effectiveParams',
1073
+ 'preferredBackend',
1074
+ 'usedBackend',
1075
+ 'attempts',
1076
+ 'durationMs',
1077
+ 'exitCode',
1078
+ 'timedOut',
1079
+ 'passedTests',
1080
+ 'failedTests',
1081
+ 'completedCallLabels',
1082
+ 'reason',
1083
+ 'error',
1084
+ ] as const;
1085
+ const source = record as Record<string, unknown>;
1086
+ const navigation: Record<string, unknown> = {};
1087
+ for (const key of navigationKeys) {
1088
+ if (source[key] !== undefined) navigation[key] = compactVerifierValue(source[key], 0);
1089
+ }
1090
+ return {
1091
+ ...navigation,
1092
+ payloadPreview: clipVerifierString(serialized, MAX_PROMPT_EVIDENCE_RECORD_CHARS),
1093
+ promptCompacted: true,
1094
+ note: `Full receipt remains in ${LIVE_VERIFICATION_EVIDENCE_FILE}.`,
1095
+ };
1096
+ }
1097
+
1098
+ function compactVerifierValue(value: unknown, depth: number): unknown {
1099
+ if (typeof value === 'string') return clipVerifierString(value, 2_000);
1100
+ if (value === null || typeof value !== 'object') return value;
1101
+ if (depth >= 6) return clipVerifierString(JSON.stringify(value), 2_000);
1102
+ if (Array.isArray(value)) {
1103
+ if (value.length <= 4) return value.map((item) => compactVerifierValue(item, depth + 1));
1104
+ return [
1105
+ ...value.slice(0, 3).map((item) => compactVerifierValue(item, depth + 1)),
1106
+ { promptCompactedItems: value.length - 4 },
1107
+ compactVerifierValue(value.at(-1), depth + 1),
1108
+ ];
1109
+ }
1110
+ return Object.fromEntries(
1111
+ Object.entries(value as Record<string, unknown>).map(([key, item]) => [
1112
+ key,
1113
+ compactVerifierValue(item, depth + 1),
1114
+ ]),
1115
+ );
1116
+ }
1117
+
1118
+ function clipVerifierString(value: string, maxChars: number): string {
1119
+ const redacted = redactFreeformText(value).redacted;
1120
+ if (redacted.length <= maxChars) return redacted;
1121
+ const headChars = Math.ceil(maxChars * 0.6);
1122
+ const tailChars = Math.floor(maxChars * 0.4);
1123
+ return `${redacted.slice(0, headChars)}\n...[prompt preview truncated ${redacted.length - maxChars} chars]...\n${redacted.slice(-tailChars)}`;
1124
+ }
1125
+
965
1126
  function creditBackendPreparation(
966
1127
  budget: { creditedCount: number; creditMs: number },
967
1128
  durationMs: number,
@@ -627,9 +627,9 @@ async function traceAnalyze(
627
627
  throw err;
628
628
  }
629
629
  // Providers report `inputTokens` as the *uncached* input only; the cached
630
- // portion lives in the cache fields. `llmCostAttributes` expects the TOTAL
631
- // prompt tokens (it derives uncached = total − cacheRead − cacheWrite), so
632
- // sum them here. A real total is also large enough to clear the
630
+ // portion lives in the cache fields. Phoenix expects the TOTAL prompt plus
631
+ // the cache breakdown so it can calculate cost server-side, so sum them
632
+ // here. A real total is also large enough to clear the
633
633
  // resolveTraceTokenCount sanity check, so cache-hit calls stop falling back
634
634
  // to the chars/4 estimate.
635
635
  const cacheReadTokens = result.cacheReadInputTokens ?? undefined;
@@ -1254,6 +1254,10 @@ export function preferredVerificationModel(provider: ProviderName): string {
1254
1254
  throw new Error(`Provider ${provider} does not support live semantic verification`);
1255
1255
  }
1256
1256
 
1257
+ /** The semantic reviewer intentionally uses a provider independent from the
1258
+ * compiler. Claude/Sonnet compilation therefore still receives a Terra review. */
1259
+ export const DEFAULT_VERIFICATION_PROVIDER: ProviderName = 'codex-cli';
1260
+
1257
1261
  interface ModelOption {
1258
1262
  model: string;
1259
1263
  isDefault: boolean;