imprint-mcp 0.2.1 → 0.3.0
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 +165 -201
- package/examples/discoverandgo/README.md +1 -1
- package/examples/echo/README.md +1 -1
- package/examples/google-flights/README.md +28 -0
- package/examples/google-flights/_shared/batchexecute.ts +63 -0
- package/examples/google-flights/_shared/flights_request.ts +95 -0
- package/examples/google-flights/_shared/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/index.ts +159 -0
- package/examples/google-flights/get_flight_booking_details/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/parser.ts +182 -0
- package/examples/google-flights/get_flight_booking_details/playbook.yaml +138 -0
- package/examples/google-flights/get_flight_booking_details/request-transform.ts +86 -0
- package/examples/google-flights/get_flight_booking_details/workflow.json +98 -0
- package/examples/google-flights/get_flight_calendar_prices/index.ts +131 -0
- package/examples/google-flights/get_flight_calendar_prices/package.json +9 -0
- package/examples/google-flights/get_flight_calendar_prices/parser.ts +86 -0
- package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +97 -0
- package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +31 -0
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +76 -0
- package/examples/google-flights/lookup_airport/index.ts +101 -0
- package/examples/google-flights/lookup_airport/package.json +9 -0
- package/examples/google-flights/lookup_airport/parser.ts +66 -0
- package/examples/google-flights/lookup_airport/playbook.yaml +47 -0
- package/examples/google-flights/lookup_airport/request-transform.ts +20 -0
- package/examples/google-flights/lookup_airport/workflow.json +57 -0
- package/examples/google-flights/search_flights/index.ts +219 -0
- package/examples/google-flights/search_flights/package.json +9 -0
- package/examples/google-flights/search_flights/parser.ts +169 -0
- package/examples/google-flights/search_flights/playbook.yaml +184 -0
- package/examples/google-flights/search_flights/request-transform.ts +119 -0
- package/examples/google-flights/search_flights/workflow.json +143 -0
- package/examples/google-hotels/README.md +29 -0
- package/examples/google-hotels/_shared/batchexecute.ts +73 -0
- package/examples/google-hotels/_shared/freq.ts +158 -0
- package/examples/google-hotels/_shared/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/index.ts +80 -0
- package/examples/google-hotels/autocomplete_hotel_location/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/parser.ts +71 -0
- package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +36 -0
- package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +37 -0
- package/examples/google-hotels/autocomplete_hotel_location/workflow.json +36 -0
- package/examples/google-hotels/get_hotel_booking_options/index.ts +143 -0
- package/examples/google-hotels/get_hotel_booking_options/package.json +9 -0
- package/examples/google-hotels/get_hotel_booking_options/parser.ts +271 -0
- package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +154 -0
- package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +154 -0
- package/examples/google-hotels/get_hotel_booking_options/workflow.json +84 -0
- package/examples/google-hotels/get_hotel_reviews/index.ts +81 -0
- package/examples/google-hotels/get_hotel_reviews/package.json +9 -0
- package/examples/google-hotels/get_hotel_reviews/parser.ts +128 -0
- package/examples/google-hotels/get_hotel_reviews/playbook.yaml +64 -0
- package/examples/google-hotels/get_hotel_reviews/request-transform.ts +42 -0
- package/examples/google-hotels/get_hotel_reviews/workflow.json +37 -0
- package/examples/google-hotels/search_hotels/index.ts +207 -0
- package/examples/google-hotels/search_hotels/package.json +9 -0
- package/examples/google-hotels/search_hotels/parser.ts +260 -0
- package/examples/google-hotels/search_hotels/playbook.yaml +87 -0
- package/examples/google-hotels/search_hotels/request-transform.ts +197 -0
- package/examples/google-hotels/search_hotels/workflow.json +127 -0
- package/package.json +3 -2
- package/prompts/audit-agent.md +71 -0
- package/prompts/build-planning.md +74 -0
- package/prompts/compile-agent.md +131 -27
- package/prompts/prereq-builder.md +64 -0
- package/prompts/prereq-planner.md +34 -0
- package/prompts/tool-planning.md +39 -0
- package/src/cli.ts +109 -2
- package/src/imprint/agent.ts +5 -0
- package/src/imprint/audit.ts +996 -0
- package/src/imprint/backend-ladder.ts +1214 -184
- package/src/imprint/build-plan.ts +1051 -0
- package/src/imprint/cdp-browser-fetch.ts +589 -0
- package/src/imprint/cdp-jar-cache.ts +320 -0
- package/src/imprint/chromium.ts +135 -0
- package/src/imprint/claude-cli-compile.ts +125 -25
- package/src/imprint/codex-cli-compile.ts +26 -23
- package/src/imprint/compile-agent-types.ts +38 -0
- package/src/imprint/compile-agent.ts +63 -25
- package/src/imprint/compile-tools.ts +1656 -64
- package/src/imprint/compile.ts +13 -1
- package/src/imprint/concurrency.ts +87 -0
- package/src/imprint/cron.ts +1 -0
- package/src/imprint/doctor.ts +39 -0
- package/src/imprint/freeform-redact.ts +5 -4
- package/src/imprint/integrations.ts +2 -2
- package/src/imprint/llm.ts +56 -8
- package/src/imprint/mcp-compile-server.ts +43 -10
- package/src/imprint/mcp-maintenance.ts +9 -101
- package/src/imprint/mcp-server.ts +73 -7
- package/src/imprint/multi-progress.ts +7 -2
- package/src/imprint/param-grounding.ts +367 -0
- package/src/imprint/paths.ts +29 -0
- package/src/imprint/playbook-runner.ts +101 -40
- package/src/imprint/prereq-builder.ts +651 -0
- package/src/imprint/probe-backends.ts +6 -3
- package/src/imprint/record.ts +10 -1
- package/src/imprint/redact.ts +30 -2
- package/src/imprint/replay-capture.ts +19 -18
- package/src/imprint/runtime.ts +19 -10
- package/src/imprint/session-diff.ts +79 -2
- package/src/imprint/session-merge.ts +9 -5
- package/src/imprint/stealth-chromium.ts +81 -0
- package/src/imprint/stealth-fetch.ts +309 -29
- package/src/imprint/stealth-token-cache.ts +88 -0
- package/src/imprint/teach-plan.ts +251 -0
- package/src/imprint/teach-state.ts +10 -0
- package/src/imprint/teach.ts +456 -142
- package/src/imprint/tool-candidates.ts +72 -14
- package/src/imprint/tool-plan.ts +313 -0
- package/src/imprint/tracing.ts +135 -6
- package/src/imprint/types.ts +61 -3
- package/examples/google-flights/search_google_flights/index.ts +0 -101
- package/examples/google-flights/search_google_flights/parser.test.ts +0 -140
- package/examples/google-flights/search_google_flights/parser.ts +0 -189
- package/examples/google-flights/search_google_flights/playbook.yaml +0 -130
- package/examples/google-flights/search_google_flights/workflow.json +0 -48
- package/examples/google-hotels/search_google_hotels/index.ts +0 -194
- package/examples/google-hotels/search_google_hotels/parser.test.ts +0 -168
- package/examples/google-hotels/search_google_hotels/parser.ts +0 -330
- package/examples/google-hotels/search_google_hotels/playbook.yaml +0 -125
- package/examples/google-hotels/search_google_hotels/workflow.json +0 -111
- package/examples/namecheap-domains/search_namecheap_domains/index.ts +0 -144
- package/examples/namecheap-domains/search_namecheap_domains/parser.ts +0 -380
- package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +0 -50
- package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +0 -136
- package/examples/namecheap-domains/search_namecheap_domains/workflow.json +0 -97
|
@@ -28,7 +28,6 @@ import * as p from '@clack/prompts';
|
|
|
28
28
|
import YAML from 'yaml';
|
|
29
29
|
import { imprintHomeDir, localSiteDir } from './paths.ts';
|
|
30
30
|
import {
|
|
31
|
-
type TeachState,
|
|
32
31
|
type WorkflowState,
|
|
33
32
|
loadTeachState,
|
|
34
33
|
resolveTeachStatePath,
|
|
@@ -38,7 +37,7 @@ import {
|
|
|
38
37
|
|
|
39
38
|
type McpClient = 'claude-code' | 'codex' | 'claude-desktop' | 'openclaw' | 'hermes';
|
|
40
39
|
type LocalDeleteMode = 'none' | 'tool' | 'site';
|
|
41
|
-
type IssueKind = 'incomplete' | 'missing-session' | '
|
|
40
|
+
type IssueKind = 'incomplete' | 'missing-session' | 'stale-registration';
|
|
42
41
|
|
|
43
42
|
const CLIENTS: McpClient[] = ['claude-code', 'codex', 'claude-desktop', 'openclaw', 'hermes'];
|
|
44
43
|
const DISABLED_STORE_VERSION = 1;
|
|
@@ -100,7 +99,6 @@ interface LocalSiteStatus {
|
|
|
100
99
|
dir: string;
|
|
101
100
|
tools: LocalToolStatus[];
|
|
102
101
|
workflows: LocalWorkflowStatus[];
|
|
103
|
-
orphanSessions: string[];
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
interface McpIssue {
|
|
@@ -393,22 +391,6 @@ async function runInteractiveIssueFix(status: McpStatus, ctx: MaintenanceContext
|
|
|
393
391
|
return;
|
|
394
392
|
}
|
|
395
393
|
|
|
396
|
-
const orphanIssues = indices
|
|
397
|
-
.map((i) => status.issues[i])
|
|
398
|
-
.filter(
|
|
399
|
-
(issue): issue is McpIssue => !!issue && issue.kind === 'orphan-session' && !!issue.path,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
let deleteOrphans = true;
|
|
403
|
-
if (orphanIssues.length > 0) {
|
|
404
|
-
const confirm = await p.confirm({
|
|
405
|
-
message: `Delete ${orphanIssues.length} orphan session file${orphanIssues.length === 1 ? '' : 's'}?`,
|
|
406
|
-
initialValue: false,
|
|
407
|
-
});
|
|
408
|
-
if (p.isCancel(confirm)) return;
|
|
409
|
-
deleteOrphans = confirm === true;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
394
|
const aggregate: MutationResult = { changed: [], skipped: [] };
|
|
413
395
|
for (const index of indices) {
|
|
414
396
|
const issue = status.issues[index];
|
|
@@ -416,17 +398,12 @@ async function runInteractiveIssueFix(status: McpStatus, ctx: MaintenanceContext
|
|
|
416
398
|
appendMutation(aggregate, { changed: [], skipped: ['selection disappeared'] });
|
|
417
399
|
continue;
|
|
418
400
|
}
|
|
419
|
-
appendMutation(aggregate, fixIssue(issue, status, ctx
|
|
401
|
+
appendMutation(aggregate, fixIssue(issue, status, ctx));
|
|
420
402
|
}
|
|
421
403
|
reportMutation(aggregate);
|
|
422
404
|
}
|
|
423
405
|
|
|
424
|
-
function fixIssue(
|
|
425
|
-
issue: McpIssue,
|
|
426
|
-
status: McpStatus,
|
|
427
|
-
ctx: MaintenanceContext,
|
|
428
|
-
opts: { deleteOrphans: boolean },
|
|
429
|
-
): MutationResult {
|
|
406
|
+
function fixIssue(issue: McpIssue, status: McpStatus, ctx: MaintenanceContext): MutationResult {
|
|
430
407
|
if (issue.kind === 'stale-registration') {
|
|
431
408
|
const reg = status.registrations.find(
|
|
432
409
|
(r) =>
|
|
@@ -446,13 +423,6 @@ function fixIssue(
|
|
|
446
423
|
return pruneSingleTeachWorkflow(issue.site, issue.workflow);
|
|
447
424
|
}
|
|
448
425
|
|
|
449
|
-
if (issue.kind === 'orphan-session' && issue.path) {
|
|
450
|
-
if (!opts.deleteOrphans) {
|
|
451
|
-
return { changed: [], skipped: [`kept orphan session ${issue.path}`] };
|
|
452
|
-
}
|
|
453
|
-
return deleteOrphanSessionFile(issue.path);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
426
|
return { changed: [], skipped: [`no automatic fix for ${issue.kind}`] };
|
|
457
427
|
}
|
|
458
428
|
|
|
@@ -463,7 +433,7 @@ async function runInteractiveLocalDelete(status: McpStatus): Promise<void> {
|
|
|
463
433
|
const complete = s.tools.filter((t) => t.complete).length;
|
|
464
434
|
return {
|
|
465
435
|
value: s.site,
|
|
466
|
-
label: `${s.site} (${complete} complete tool${complete === 1 ? '' : 's'}
|
|
436
|
+
label: `${s.site} (${complete} complete tool${complete === 1 ? '' : 's'})`,
|
|
467
437
|
};
|
|
468
438
|
}),
|
|
469
439
|
});
|
|
@@ -686,7 +656,7 @@ function formatMcpStatus(status: McpStatus): string {
|
|
|
686
656
|
const incomplete = s.workflows.filter((w) => w.incomplete).length;
|
|
687
657
|
const missing = s.workflows.filter((w) => w.missingSession).length;
|
|
688
658
|
lines.push(
|
|
689
|
-
` ${s.site}: ${complete} complete tool${complete === 1 ? '' : 's'}, ${incomplete} incomplete workflow${incomplete === 1 ? '' : 's'}, ${missing} missing-session issue${missing === 1 ? '' : 's'}
|
|
659
|
+
` ${s.site}: ${complete} complete tool${complete === 1 ? '' : 's'}, ${incomplete} incomplete workflow${incomplete === 1 ? '' : 's'}, ${missing} missing-session issue${missing === 1 ? '' : 's'}`,
|
|
690
660
|
);
|
|
691
661
|
}
|
|
692
662
|
}
|
|
@@ -714,8 +684,6 @@ function issueFixHint(issue: McpIssue): string | null {
|
|
|
714
684
|
return `choose "Fix an issue" or run: imprint mcp prune-state --site ${issue.site} --incomplete --yes`;
|
|
715
685
|
case 'missing-session':
|
|
716
686
|
return `choose "Fix an issue" or run: imprint mcp prune-state --site ${issue.site} --missing-session --yes`;
|
|
717
|
-
case 'orphan-session':
|
|
718
|
-
return 'choose "Fix an issue" to delete this recording, or keep it if you still need it';
|
|
719
687
|
}
|
|
720
688
|
return null;
|
|
721
689
|
}
|
|
@@ -737,12 +705,12 @@ function scanLocalSites(ctx: MaintenanceContext): LocalSiteStatus[] {
|
|
|
737
705
|
if (entry === 'node_modules' || entry.startsWith('.')) continue;
|
|
738
706
|
const dir = pathJoin(ctx.imprintHome, entry);
|
|
739
707
|
if (!safeIsDir(dir)) continue;
|
|
740
|
-
sites.push(scanLocalSite(
|
|
708
|
+
sites.push(scanLocalSite(entry, dir));
|
|
741
709
|
}
|
|
742
710
|
return sites;
|
|
743
711
|
}
|
|
744
712
|
|
|
745
|
-
function scanLocalSite(
|
|
713
|
+
function scanLocalSite(site: string, dir: string): LocalSiteStatus {
|
|
746
714
|
const tools: LocalToolStatus[] = [];
|
|
747
715
|
for (const entry of readdirSync(dir).sort()) {
|
|
748
716
|
if (entry === 'sessions' || entry === '_shared' || entry.startsWith('.')) continue;
|
|
@@ -764,12 +732,8 @@ function scanLocalSite(ctx: MaintenanceContext, site: string, dir: string): Loca
|
|
|
764
732
|
const workflows = Object.entries(state.workflows)
|
|
765
733
|
.map(([name, ws]) => workflowStatus(site, name, ws, tools))
|
|
766
734
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
767
|
-
const referenced = referencedSessionPaths(site, state);
|
|
768
|
-
const orphanSessions = discoverSessionFiles(pathJoin(dir, 'sessions')).filter(
|
|
769
|
-
(session) => !isReferencedSessionFile(site, session, ctx, referenced),
|
|
770
|
-
);
|
|
771
735
|
|
|
772
|
-
return { site, dir, tools, workflows
|
|
736
|
+
return { site, dir, tools, workflows };
|
|
773
737
|
}
|
|
774
738
|
|
|
775
739
|
function workflowStatus(
|
|
@@ -813,47 +777,6 @@ function workflowJsonToolName(toolDir: string): string | null {
|
|
|
813
777
|
}
|
|
814
778
|
}
|
|
815
779
|
|
|
816
|
-
function referencedSessionPaths(site: string, state: TeachState): Set<string> {
|
|
817
|
-
const out = new Set<string>();
|
|
818
|
-
for (const ws of Object.values(state.workflows)) {
|
|
819
|
-
for (const stored of [ws.sessionPath, ws.redactedPath, ws.triagedPath]) {
|
|
820
|
-
if (!stored) continue;
|
|
821
|
-
out.add(stored);
|
|
822
|
-
const resolved = resolveTeachStatePath(site, stored);
|
|
823
|
-
if (resolved) out.add(resolved);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return out;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function discoverSessionFiles(sessionDir: string): string[] {
|
|
830
|
-
if (!existsSync(sessionDir)) return [];
|
|
831
|
-
return readdirSync(sessionDir)
|
|
832
|
-
.filter((f) => (f.endsWith('.json') || f.endsWith('.jsonl')) && !f.includes('.triaged'))
|
|
833
|
-
.map((f) => pathJoin(sessionDir, f))
|
|
834
|
-
.sort();
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function isReferencedSessionFile(
|
|
838
|
-
site: string,
|
|
839
|
-
absolutePath: string,
|
|
840
|
-
ctx: MaintenanceContext,
|
|
841
|
-
referenced: Set<string>,
|
|
842
|
-
): boolean {
|
|
843
|
-
const candidates = [absolutePath, relativeToSite(site, absolutePath, ctx)];
|
|
844
|
-
if (absolutePath.endsWith('.jsonl')) {
|
|
845
|
-
const jsonPath = absolutePath.replace(/\.jsonl$/, '.json');
|
|
846
|
-
candidates.push(jsonPath, relativeToSite(site, jsonPath, ctx));
|
|
847
|
-
}
|
|
848
|
-
return candidates.some((candidate) => referenced.has(candidate));
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function relativeToSite(site: string, absolutePath: string, ctx: MaintenanceContext): string {
|
|
852
|
-
const siteDir = pathJoin(ctx.imprintHome, site);
|
|
853
|
-
const prefix = `${siteDir}/`;
|
|
854
|
-
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
780
|
function collectIssues(opts: {
|
|
858
781
|
registrations: McpRegistration[];
|
|
859
782
|
sites: LocalSiteStatus[];
|
|
@@ -881,14 +804,6 @@ function collectIssues(opts: {
|
|
|
881
804
|
});
|
|
882
805
|
}
|
|
883
806
|
}
|
|
884
|
-
for (const session of site.orphanSessions) {
|
|
885
|
-
issues.push({
|
|
886
|
-
kind: 'orphan-session',
|
|
887
|
-
site: site.site,
|
|
888
|
-
message: `${site.site} has an untracked session ${session}`,
|
|
889
|
-
path: session,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
807
|
}
|
|
893
808
|
|
|
894
809
|
for (const r of opts.registrations) {
|
|
@@ -1174,7 +1089,7 @@ function pruneTeachState(
|
|
|
1174
1089
|
for (const site of sites) {
|
|
1175
1090
|
const statePath = teachStatePath(site);
|
|
1176
1091
|
if (!existsSync(statePath)) continue;
|
|
1177
|
-
const status = scanLocalSite(
|
|
1092
|
+
const status = scanLocalSite(site, localSiteDir(site));
|
|
1178
1093
|
const remove = new Set(
|
|
1179
1094
|
status.workflows
|
|
1180
1095
|
.filter(
|
|
@@ -1208,13 +1123,6 @@ function pruneSingleTeachWorkflow(site: string, workflow: string): MutationResul
|
|
|
1208
1123
|
return { changed: [`pruned teach-state entry ${site}/${workflow}`], skipped: [] };
|
|
1209
1124
|
}
|
|
1210
1125
|
|
|
1211
|
-
function deleteOrphanSessionFile(path: string): MutationResult {
|
|
1212
|
-
if (!existsSync(path))
|
|
1213
|
-
return { changed: [], skipped: [`orphan session ${path} no longer exists`] };
|
|
1214
|
-
rmSync(path, { force: true });
|
|
1215
|
-
return { changed: [`deleted orphan session ${path}`], skipped: [] };
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
1126
|
function matchesTarget(
|
|
1219
1127
|
reg: McpRegistration,
|
|
1220
1128
|
target: string,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type Tool,
|
|
18
18
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
19
19
|
import { resolveLadder, runWithLadder } from './backend-ladder.ts';
|
|
20
|
+
import type { CdpBrowserFetch } from './cdp-browser-fetch.ts';
|
|
20
21
|
import { createLog } from './log.ts';
|
|
21
22
|
import { imprintHomeDir } from './paths.ts';
|
|
22
23
|
import { loadBackendsCache } from './probe-backends.ts';
|
|
@@ -66,11 +67,17 @@ function buildToolDescription(w: ResolvedTool['workflow']): string {
|
|
|
66
67
|
|
|
67
68
|
/** MCP advertises tool input as JSON Schema; build it directly from
|
|
68
69
|
* workflow parameters rather than going through Zod. */
|
|
69
|
-
function buildJsonSchema(parameters: WorkflowParameter[]): Tool['inputSchema'] {
|
|
70
|
+
export function buildJsonSchema(parameters: WorkflowParameter[]): Tool['inputSchema'] {
|
|
70
71
|
const properties: Record<string, { type: string; description: string }> = {};
|
|
71
72
|
const required: string[] = [];
|
|
72
73
|
for (const p of parameters) {
|
|
73
|
-
|
|
74
|
+
// Producer-sourced token params: tell the orchestrating LLM where to mint the
|
|
75
|
+
// value so it calls the producer once and reuses it, rather than fabricating
|
|
76
|
+
// an opaque token (which the tool would reject).
|
|
77
|
+
const description = p.sourcedFrom
|
|
78
|
+
? `${p.description} Obtain this value from the \`${p.sourcedFrom.tool}\` tool's \`${p.sourcedFrom.field}\` output — call \`${p.sourcedFrom.tool}\` first and reuse the value across calls (no need to re-fetch each time).`
|
|
79
|
+
: p.description;
|
|
80
|
+
properties[p.name] = { type: p.type, description };
|
|
74
81
|
if (p.default === undefined) required.push(p.name);
|
|
75
82
|
}
|
|
76
83
|
return {
|
|
@@ -88,13 +95,13 @@ function buildServer(
|
|
|
88
95
|
version: string,
|
|
89
96
|
tools: ResolvedTool[],
|
|
90
97
|
assetRoot: string,
|
|
91
|
-
): Server {
|
|
98
|
+
): { server: Server; closeCdpPool: () => Promise<void> } {
|
|
92
99
|
const server = new Server(
|
|
93
100
|
{ name, version },
|
|
94
101
|
{
|
|
95
102
|
capabilities: { tools: {} },
|
|
96
103
|
instructions:
|
|
97
|
-
'Imprint runs deterministic workflows captured from real browser sessions. Tools prefer fetch API replay, may use gated fetch-bootstrap only for declared browser-minted state, then stealth-fetch for bot-defense state, and playbook only for full DOM interaction. Error codes: AUTH_EXPIRED (401, run `imprint login <site>`); STATE_MISSING (required cookie/state was unavailable or ambiguous); FORBIDDEN (403); RATE_LIMITED (429, back off); BAD_RESPONSE (other 4xx/5xx); NETWORK (fetch failed); UNKNOWN (everything else).',
|
|
104
|
+
'Imprint runs deterministic workflows captured from real browser sessions. Tools prefer fetch API replay, may use gated fetch-bootstrap only for declared browser-minted state, then cdp-replay (API requests run inside a live trusted Chrome so a protected POST refreshes its anti-bot token between calls) for multi-step state-changing flows, then stealth-fetch for bot-defense state, and playbook only for full DOM interaction. Error codes: AUTH_EXPIRED (401, run `imprint login <site>`); STATE_MISSING (required cookie/state was unavailable or ambiguous); FORBIDDEN (403); RATE_LIMITED (429, back off); BAD_RESPONSE (other 4xx/5xx); NETWORK (fetch failed); UNKNOWN (everything else).',
|
|
98
105
|
},
|
|
99
106
|
);
|
|
100
107
|
|
|
@@ -105,6 +112,21 @@ function buildServer(
|
|
|
105
112
|
// Per-site stealth-fetch cache so the ~12s bootstrap runs once per site.
|
|
106
113
|
const stealthCache = new Map<string, StealthFetch>();
|
|
107
114
|
|
|
115
|
+
// Per-site CDP browser pool: cdp-replay stores its live Chrome here after
|
|
116
|
+
// the first successful call so subsequent calls reuse it (~2-5s vs ~33s).
|
|
117
|
+
const cdpPool = new Map<string, CdpBrowserFetch>();
|
|
118
|
+
const cdpIdleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
119
|
+
const CDP_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
120
|
+
|
|
121
|
+
// Per-tool memo of the winning backend for THIS server session. After the
|
|
122
|
+
// first call discovers the right rung, later calls skip the doomed early ones
|
|
123
|
+
// (e.g. southwest's ~80s fetch-bootstrap FORBIDDEN before cdp-replay wins). Its
|
|
124
|
+
// lifetime is tied to `cdpPool`: the memoized cdp-replay is only cheap while
|
|
125
|
+
// its Chrome is pooled, so a site's memo is evicted when that pool entry is
|
|
126
|
+
// idle-closed (below) — otherwise the next call would start at a now-cold
|
|
127
|
+
// cdp-replay and re-pay the ~33s relaunch.
|
|
128
|
+
const winnerCache = new Map<string, ConcreteBackend>();
|
|
129
|
+
|
|
108
130
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
109
131
|
tools: tools.map((t) => ({
|
|
110
132
|
name: t.workflow.toolName,
|
|
@@ -140,6 +162,15 @@ function buildServer(
|
|
|
140
162
|
string | number | boolean
|
|
141
163
|
>;
|
|
142
164
|
|
|
165
|
+
// Audit-only pacing: when the audit harness sets IMPRINT_AUDIT_PACING_MS,
|
|
166
|
+
// sleep before each tool call so the auditor's per-parameter differential
|
|
167
|
+
// probing of bot-defended idempotent reads stays steady enough not to trip
|
|
168
|
+
// the per-IP anti-bot defense. Unset in production → no delay.
|
|
169
|
+
const pacingMs = Number(process.env.IMPRINT_AUDIT_PACING_MS);
|
|
170
|
+
if (Number.isFinite(pacingMs) && pacingMs > 0) {
|
|
171
|
+
await new Promise((r) => setTimeout(r, pacingMs));
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
try {
|
|
144
175
|
const ladder = resolveLadder('auto', tool.preferredOrder);
|
|
145
176
|
const { result, usedBackend } = await runWithLadder(
|
|
@@ -148,7 +179,29 @@ function buildServer(
|
|
|
148
179
|
args,
|
|
149
180
|
assetRoot,
|
|
150
181
|
stealthCache,
|
|
182
|
+
{ cdpPool, winnerCache },
|
|
151
183
|
);
|
|
184
|
+
// Reset the idle timer for this site's pooled Chrome.
|
|
185
|
+
if (result.ok && usedBackend === 'cdp-replay' && cdpPool.has(tool.site)) {
|
|
186
|
+
const prev = cdpIdleTimers.get(tool.site);
|
|
187
|
+
if (prev) clearTimeout(prev);
|
|
188
|
+
const timer = setTimeout(() => {
|
|
189
|
+
const cf = cdpPool.get(tool.site);
|
|
190
|
+
if (cf) {
|
|
191
|
+
log(`closing idle CDP session for ${tool.site}`);
|
|
192
|
+
cf.close().catch(() => {});
|
|
193
|
+
cdpPool.delete(tool.site);
|
|
194
|
+
cdpIdleTimers.delete(tool.site);
|
|
195
|
+
// Drop this site's winner memo too: a memoized cdp-replay would now
|
|
196
|
+
// point at a closed Chrome and re-pay the cold relaunch.
|
|
197
|
+
for (const key of winnerCache.keys()) {
|
|
198
|
+
if (key.startsWith(`${tool.site}:`)) winnerCache.delete(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}, CDP_IDLE_TIMEOUT_MS);
|
|
202
|
+
timer.unref();
|
|
203
|
+
cdpIdleTimers.set(tool.site, timer);
|
|
204
|
+
}
|
|
152
205
|
if (!result.ok) {
|
|
153
206
|
const text = formatToolError(result);
|
|
154
207
|
return {
|
|
@@ -165,7 +218,18 @@ function buildServer(
|
|
|
165
218
|
}
|
|
166
219
|
});
|
|
167
220
|
|
|
168
|
-
|
|
221
|
+
async function closeCdpPool(): Promise<void> {
|
|
222
|
+
for (const [site, cf] of cdpPool) {
|
|
223
|
+
log(`shutdown: closing CDP session for ${site}`);
|
|
224
|
+
await cf.close().catch(() => {});
|
|
225
|
+
}
|
|
226
|
+
cdpPool.clear();
|
|
227
|
+
for (const timer of cdpIdleTimers.values()) clearTimeout(timer);
|
|
228
|
+
cdpIdleTimers.clear();
|
|
229
|
+
winnerCache.clear();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { server, closeCdpPool };
|
|
169
233
|
}
|
|
170
234
|
|
|
171
235
|
function formatToolError(result: Extract<ToolResult, { ok: false }>): string {
|
|
@@ -263,7 +327,7 @@ async function runStdio(
|
|
|
263
327
|
tools: ResolvedTool[],
|
|
264
328
|
assetRoot: string,
|
|
265
329
|
): Promise<void> {
|
|
266
|
-
const server = buildServer(name, version, tools, assetRoot);
|
|
330
|
+
const { server, closeCdpPool } = buildServer(name, version, tools, assetRoot);
|
|
267
331
|
const transport = new StdioServerTransport();
|
|
268
332
|
await server.connect(transport);
|
|
269
333
|
log(`stdio transport ready (${tools.length} tool${tools.length === 1 ? '' : 's'})`);
|
|
@@ -277,6 +341,7 @@ async function runStdio(
|
|
|
277
341
|
process.once('SIGINT', () => done('SIGINT'));
|
|
278
342
|
process.once('SIGTERM', () => done('SIGTERM'));
|
|
279
343
|
});
|
|
344
|
+
await closeCdpPool();
|
|
280
345
|
}
|
|
281
346
|
|
|
282
347
|
/**
|
|
@@ -296,7 +361,7 @@ async function runHttp(
|
|
|
296
361
|
port: number,
|
|
297
362
|
assetRoot: string,
|
|
298
363
|
): Promise<void> {
|
|
299
|
-
const server = buildServer(name, version, tools, assetRoot);
|
|
364
|
+
const { server, closeCdpPool } = buildServer(name, version, tools, assetRoot);
|
|
300
365
|
const transport = new StreamableHTTPServerTransport({
|
|
301
366
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
302
367
|
});
|
|
@@ -347,4 +412,5 @@ async function runHttp(
|
|
|
347
412
|
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
348
413
|
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
349
414
|
});
|
|
415
|
+
await closeCdpPool();
|
|
350
416
|
}
|
|
@@ -55,15 +55,20 @@ export class MultiProgress {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
private redraw(): void {
|
|
58
|
+
const cols = process.stderr.columns || 80;
|
|
58
59
|
let buf = '';
|
|
59
60
|
if (this.renderedCount > 0) {
|
|
60
61
|
buf += `\x1b[${this.renderedCount}F`;
|
|
61
62
|
}
|
|
62
63
|
buf += '\x1b[J';
|
|
64
|
+
let physicalLines = 0;
|
|
63
65
|
for (const [, msg] of this.lines) {
|
|
64
|
-
|
|
66
|
+
const line = `│ ${msg}`;
|
|
67
|
+
const truncated = line.length >= cols ? line.slice(0, cols - 1) : line;
|
|
68
|
+
buf += `${truncated}\n`;
|
|
69
|
+
physicalLines += 1;
|
|
65
70
|
}
|
|
66
71
|
process.stderr.write(buf);
|
|
67
|
-
this.renderedCount =
|
|
72
|
+
this.renderedCount = physicalLines;
|
|
68
73
|
}
|
|
69
74
|
}
|