imprint-mcp 0.2.0 → 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 +132 -28
- 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 +111 -4
- 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 +65 -27
- package/src/imprint/compile-tools.ts +1656 -64
- package/src/imprint/compile.ts +14 -2
- package/src/imprint/concurrency.ts +87 -0
- package/src/imprint/credential-extract.ts +174 -25
- package/src/imprint/cron.ts +1 -0
- package/src/imprint/doctor.ts +39 -0
- package/src/imprint/emit.ts +85 -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/sensitive-keys.ts +141 -7
- 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 +17 -0
- package/src/imprint/teach.ts +582 -147
- 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
package/src/imprint/llm.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
llmSpanAttributes,
|
|
7
7
|
resolveTraceTokenCount,
|
|
8
8
|
setSpanAttributes,
|
|
9
|
+
totalPromptTokens,
|
|
9
10
|
traceLlmIoEnabled,
|
|
10
11
|
traceLlmMessages,
|
|
11
12
|
traced,
|
|
@@ -17,6 +18,16 @@ interface AnalyzeResult {
|
|
|
17
18
|
text: string;
|
|
18
19
|
inputTokens: number | null;
|
|
19
20
|
outputTokens: number | null;
|
|
21
|
+
/**
|
|
22
|
+
* Prompt-cache token counts, when the provider reports them. `inputTokens` is
|
|
23
|
+
* the *uncached* input only (the Anthropic/CLI `usage.input_tokens`); the bulk
|
|
24
|
+
* of a cache-hit call lives here. Threaded through so `llm.analyze` cost is
|
|
25
|
+
* cache-aware (cache reads bill at 0.1×, writes at 1.25×) instead of charging
|
|
26
|
+
* the whole prompt at the full input rate. Null/undefined for providers that
|
|
27
|
+
* don't expose usage (codex-cli, cursor-cli).
|
|
28
|
+
*/
|
|
29
|
+
cacheReadInputTokens?: number | null;
|
|
30
|
+
cacheCreationInputTokens?: number | null;
|
|
20
31
|
durationMs: number;
|
|
21
32
|
stopReason: string | null;
|
|
22
33
|
}
|
|
@@ -129,6 +140,8 @@ class AnthropicApiProvider implements LLMProvider {
|
|
|
129
140
|
text,
|
|
130
141
|
inputTokens: response.usage.input_tokens,
|
|
131
142
|
outputTokens: response.usage.output_tokens,
|
|
143
|
+
cacheReadInputTokens: response.usage.cache_read_input_tokens ?? null,
|
|
144
|
+
cacheCreationInputTokens: response.usage.cache_creation_input_tokens ?? null,
|
|
132
145
|
durationMs: Date.now() - t0,
|
|
133
146
|
stopReason: response.stop_reason ?? null,
|
|
134
147
|
};
|
|
@@ -256,7 +269,15 @@ class ClaudeCliProvider implements LLMProvider {
|
|
|
256
269
|
);
|
|
257
270
|
}
|
|
258
271
|
|
|
259
|
-
let parsed: {
|
|
272
|
+
let parsed: {
|
|
273
|
+
result?: string;
|
|
274
|
+
usage?: {
|
|
275
|
+
input_tokens?: number;
|
|
276
|
+
output_tokens?: number;
|
|
277
|
+
cache_read_input_tokens?: number;
|
|
278
|
+
cache_creation_input_tokens?: number;
|
|
279
|
+
};
|
|
280
|
+
};
|
|
260
281
|
try {
|
|
261
282
|
parsed = JSON.parse(stdout);
|
|
262
283
|
} catch (parseErr) {
|
|
@@ -273,6 +294,8 @@ class ClaudeCliProvider implements LLMProvider {
|
|
|
273
294
|
text: parsed.result,
|
|
274
295
|
inputTokens: parsed.usage?.input_tokens ?? null,
|
|
275
296
|
outputTokens: parsed.usage?.output_tokens ?? null,
|
|
297
|
+
cacheReadInputTokens: parsed.usage?.cache_read_input_tokens ?? null,
|
|
298
|
+
cacheCreationInputTokens: parsed.usage?.cache_creation_input_tokens ?? null,
|
|
276
299
|
durationMs: Date.now() - t0,
|
|
277
300
|
stopReason: null,
|
|
278
301
|
};
|
|
@@ -437,7 +460,20 @@ async function traceAnalyze(
|
|
|
437
460
|
},
|
|
438
461
|
async (span) => {
|
|
439
462
|
const result = await fn();
|
|
440
|
-
|
|
463
|
+
// Providers report `inputTokens` as the *uncached* input only; the cached
|
|
464
|
+
// portion lives in the cache fields. `llmCostAttributes` expects the TOTAL
|
|
465
|
+
// prompt tokens (it derives uncached = total − cacheRead − cacheWrite), so
|
|
466
|
+
// sum them here. A real total is also large enough to clear the
|
|
467
|
+
// resolveTraceTokenCount sanity check, so cache-hit calls stop falling back
|
|
468
|
+
// to the chars/4 estimate.
|
|
469
|
+
const cacheReadTokens = result.cacheReadInputTokens ?? undefined;
|
|
470
|
+
const cacheWriteTokens = result.cacheCreationInputTokens ?? undefined;
|
|
471
|
+
const totalInputTokens = totalPromptTokens(
|
|
472
|
+
result.inputTokens,
|
|
473
|
+
cacheReadTokens,
|
|
474
|
+
cacheWriteTokens,
|
|
475
|
+
);
|
|
476
|
+
const inputTokens = resolveTraceTokenCount(totalInputTokens, details?.inputText);
|
|
441
477
|
const outputTokens = resolveTraceTokenCount(result.outputTokens, result.text);
|
|
442
478
|
setSpanAttributes(span, {
|
|
443
479
|
...llmSpanAttributes({
|
|
@@ -445,6 +481,8 @@ async function traceAnalyze(
|
|
|
445
481
|
model,
|
|
446
482
|
inputTokens: inputTokens.tokens,
|
|
447
483
|
outputTokens: outputTokens.tokens,
|
|
484
|
+
cacheReadTokens,
|
|
485
|
+
cacheWriteTokens,
|
|
448
486
|
tokenCountsEstimated:
|
|
449
487
|
inputTokens.source === 'estimated' || outputTokens.source === 'estimated',
|
|
450
488
|
inputTokenSource: inputTokens.source,
|
|
@@ -512,12 +550,20 @@ async function traceMessageWithTools(
|
|
|
512
550
|
return `[${b.type}]`;
|
|
513
551
|
})
|
|
514
552
|
.join('\n');
|
|
553
|
+
const cacheReadTokens = response.usage.cache_read_input_tokens ?? undefined;
|
|
554
|
+
const cacheWriteTokens = response.usage.cache_creation_input_tokens ?? undefined;
|
|
515
555
|
setSpanAttributes(span, {
|
|
516
556
|
...llmSpanAttributes({
|
|
517
557
|
provider,
|
|
518
558
|
model,
|
|
519
|
-
inputTokens:
|
|
559
|
+
inputTokens: totalPromptTokens(
|
|
560
|
+
response.usage.input_tokens,
|
|
561
|
+
cacheReadTokens,
|
|
562
|
+
cacheWriteTokens,
|
|
563
|
+
),
|
|
520
564
|
outputTokens: response.usage.output_tokens,
|
|
565
|
+
cacheReadTokens,
|
|
566
|
+
cacheWriteTokens,
|
|
521
567
|
stopReason: response.stop_reason,
|
|
522
568
|
outputMessages: captureIo
|
|
523
569
|
? traceLlmMessages([{ role: 'assistant', content: outputText }])
|
|
@@ -824,7 +870,7 @@ export function detectTeachProvider(): ProviderName {
|
|
|
824
870
|
}
|
|
825
871
|
|
|
826
872
|
function createProvider(name: ProviderName, opts: LLMOptions = {}): LLMProvider {
|
|
827
|
-
const model = opts.model ?? process.env.ANTHROPIC_MODEL ?? 'claude-opus-4-
|
|
873
|
+
const model = opts.model ?? process.env.ANTHROPIC_MODEL ?? 'claude-opus-4-8';
|
|
828
874
|
const temperature = opts.temperature ?? 0;
|
|
829
875
|
const maxTokens = opts.maxTokens ?? 8192;
|
|
830
876
|
|
|
@@ -865,11 +911,11 @@ export function preferredAgentModel(provider: ProviderName): string {
|
|
|
865
911
|
switch (provider) {
|
|
866
912
|
case 'anthropic-api':
|
|
867
913
|
case 'claude-cli':
|
|
868
|
-
return 'claude-opus-4-
|
|
914
|
+
return 'claude-opus-4-8';
|
|
869
915
|
case 'codex-cli':
|
|
870
916
|
return 'gpt-5.5';
|
|
871
917
|
case 'cursor-cli':
|
|
872
|
-
return 'claude-opus-4-
|
|
918
|
+
return 'claude-opus-4-8'; // best-effort; cursor passes through
|
|
873
919
|
}
|
|
874
920
|
}
|
|
875
921
|
|
|
@@ -883,7 +929,8 @@ export function availableModelsForProvider(provider: ProviderName): ModelOption[
|
|
|
883
929
|
case 'anthropic-api':
|
|
884
930
|
case 'claude-cli':
|
|
885
931
|
return [
|
|
886
|
-
{ model: 'claude-opus-4-
|
|
932
|
+
{ model: 'claude-opus-4-8', isDefault: true },
|
|
933
|
+
{ model: 'claude-opus-4-7', isDefault: false },
|
|
887
934
|
{ model: 'claude-sonnet-4-6', isDefault: false },
|
|
888
935
|
{ model: 'claude-haiku-4-5', isDefault: false },
|
|
889
936
|
{ model: 'claude-opus-4-6', isDefault: false },
|
|
@@ -908,7 +955,8 @@ export function availableModelsForProvider(provider: ProviderName): ModelOption[
|
|
|
908
955
|
];
|
|
909
956
|
case 'cursor-cli':
|
|
910
957
|
return [
|
|
911
|
-
{ model: 'claude-opus-4-
|
|
958
|
+
{ model: 'claude-opus-4-8', isDefault: true },
|
|
959
|
+
{ model: 'claude-opus-4-7', isDefault: false },
|
|
912
960
|
{ model: 'claude-sonnet-4-6', isDefault: false },
|
|
913
961
|
{ model: 'claude-haiku-4-5', isDefault: false },
|
|
914
962
|
{ model: 'gpt-5.5', isDefault: false },
|
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
ListToolsRequestSchema,
|
|
25
25
|
type Tool,
|
|
26
26
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
-
import {
|
|
27
|
+
import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
|
|
28
|
+
import {
|
|
29
|
+
applyLiveVerification,
|
|
30
|
+
applyParamVerification,
|
|
31
|
+
buildCompileTools,
|
|
32
|
+
externalVerification,
|
|
33
|
+
} from './compile-tools.ts';
|
|
28
34
|
import { loadJsonFile } from './load-json.ts';
|
|
29
35
|
import { createLog } from './log.ts';
|
|
30
36
|
import { redactSession } from './redact.ts';
|
|
@@ -43,6 +49,10 @@ interface RunCompileMcpServerOptions {
|
|
|
43
49
|
maxVerificationCycles?: number;
|
|
44
50
|
candidate?: ToolCandidate;
|
|
45
51
|
sharedContext?: SharedCompileContext;
|
|
52
|
+
/** Absolute path to the multi-tool build plan sidecar (.build-plan.json). */
|
|
53
|
+
buildPlanPath?: string;
|
|
54
|
+
/** Shared-module build manifest for this site (verified flags). */
|
|
55
|
+
sharedModules?: SharedModuleManifestEntry[];
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
const DONE_SENTINEL = '.compile-done.json';
|
|
@@ -67,12 +77,24 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
|
|
|
67
77
|
session = redactSession(session).session;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
// Build the
|
|
80
|
+
// Build the read/write tools (same as the in-process loop). When a build
|
|
81
|
+
// plan is present, buildCompileTools also exposes read_build_plan.
|
|
71
82
|
const compileTools = buildCompileTools(session, opts.toolDir, opts.sessionPath, {
|
|
72
83
|
candidate: opts.candidate,
|
|
73
84
|
sharedContext: opts.sharedContext,
|
|
85
|
+
buildPlanPath: opts.buildPlanPath,
|
|
86
|
+
sharedModules: opts.sharedModules,
|
|
74
87
|
});
|
|
75
88
|
|
|
89
|
+
// Resolve the shared modules + producer→consumer token contracts the plan
|
|
90
|
+
// assigned this tool, so verification can assert modules are imported and
|
|
91
|
+
// require a chained test for each producer-sourced token param.
|
|
92
|
+
const { assignedSharedModules, tokenParams, emittedTokens } = resolvePlanSliceFromFile(
|
|
93
|
+
opts.buildPlanPath,
|
|
94
|
+
opts.candidate?.toolName,
|
|
95
|
+
opts.sharedModules,
|
|
96
|
+
);
|
|
97
|
+
|
|
76
98
|
// The custom done/give_up tools live alongside in MCP space.
|
|
77
99
|
const doneTool: Tool = {
|
|
78
100
|
name: 'done',
|
|
@@ -136,25 +158,36 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
|
|
|
136
158
|
if (name === 'done') {
|
|
137
159
|
const summary = (args as { summary?: string }).summary ?? 'Task completed';
|
|
138
160
|
log(`done() called: ${summary}`);
|
|
139
|
-
const { failures, warnings } =
|
|
140
|
-
opts.toolDir,
|
|
141
|
-
session,
|
|
142
|
-
opts.sessionPath,
|
|
143
|
-
{
|
|
161
|
+
const { failures, warnings, paramVerification, liveVerification } =
|
|
162
|
+
await externalVerification(opts.toolDir, session, opts.sessionPath, {
|
|
144
163
|
expectedToolName: opts.candidate?.toolName,
|
|
145
164
|
likelyParams: opts.candidate?.likelyParams,
|
|
146
165
|
candidateRequestSeqs: opts.candidate?.requestSeqs,
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
// Widen Fix B's variation pool to dependency requests so a token that
|
|
167
|
+
// varies only across them and is frozen as a literal in the tool's
|
|
168
|
+
// request is caught (the cross-request session-token leak case).
|
|
169
|
+
dependencyRequestSeqs: opts.candidate?.dependencySeqs,
|
|
170
|
+
assignedSharedModules,
|
|
171
|
+
tokenParams,
|
|
172
|
+
emittedTokens,
|
|
173
|
+
});
|
|
149
174
|
if (warnings.length > 0) {
|
|
150
175
|
log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
|
|
151
176
|
}
|
|
152
177
|
if (failures.length === 0) {
|
|
178
|
+
// Persist per-parameter verified flags + the live-verification stamp
|
|
179
|
+
// onto workflow.json. Audit and teach read the stamp.
|
|
180
|
+
applyLiveVerification(opts.toolDir, liveVerification);
|
|
181
|
+
const paramWarnings = applyParamVerification(opts.toolDir, paramVerification);
|
|
182
|
+
if (paramWarnings.length > 0) {
|
|
183
|
+
log(`parameter verification:\n${paramWarnings.join('\n')}`);
|
|
184
|
+
}
|
|
185
|
+
const allWarnings = [...warnings, ...paramWarnings];
|
|
153
186
|
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
154
187
|
writeFileSync(
|
|
155
188
|
sentinel,
|
|
156
189
|
JSON.stringify(
|
|
157
|
-
{ summary, verification: 'passed', warnings, timestamp: Date.now() },
|
|
190
|
+
{ summary, verification: 'passed', warnings: allWarnings, timestamp: Date.now() },
|
|
158
191
|
null,
|
|
159
192
|
2,
|
|
160
193
|
),
|
|
@@ -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
|
}
|