pattern-mcp 0.12.1 → 0.13.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 +290 -266
- package/dist/client-connect.js +265 -0
- package/dist/index.js +308 -240
- package/dist/init-enforcement.js +1 -60
- package/dist/prompt.js +65 -0
- package/dist/telemetry.js +40 -7
- package/package.json +1 -1
package/dist/init-enforcement.js
CHANGED
|
@@ -15,68 +15,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { createInterface } from "node:readline";
|
|
19
18
|
import { deriveProjectId } from "./project-id.js";
|
|
19
|
+
import { closeRl, confirm, promptText, shellQuote } from "./prompt.js";
|
|
20
20
|
const HOOK_MARKER = "pattern-check-gate-hook";
|
|
21
|
-
// A queue-based prompt helper, not readline/promises' question() --
|
|
22
|
-
// question() only starts listening for a line *after* it's called, but
|
|
23
|
-
// with piped/non-TTY stdin (as in an automated test, or `init | cat`)
|
|
24
|
-
// every line arrives in one synchronous burst, ahead of any await
|
|
25
|
-
// cycle. Confirmed directly: two sequential `rl.question()` calls on a
|
|
26
|
-
// piped `printf 'a\nb\n'` answer only the first and hang forever on the
|
|
27
|
-
// second -- Node even logs "Detected unsettled top-level await" in that
|
|
28
|
-
// repro. The fix is a small always-listening queue: a persistent 'line'
|
|
29
|
-
// listener buffers answers that arrive before they're asked for, so
|
|
30
|
-
// `askLine` either drains an already-buffered answer immediately or
|
|
31
|
-
// waits for the next 'line' event, whichever comes first -- correct for
|
|
32
|
-
// both a real interactive TTY (waiter path) and piped/scripted input
|
|
33
|
-
// (queue path).
|
|
34
|
-
let rl = null;
|
|
35
|
-
const lineQueue = [];
|
|
36
|
-
const waiters = [];
|
|
37
|
-
function ensureRl() {
|
|
38
|
-
if (!rl) {
|
|
39
|
-
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
-
rl.on("line", (line) => {
|
|
41
|
-
const waiter = waiters.shift();
|
|
42
|
-
if (waiter)
|
|
43
|
-
waiter(line);
|
|
44
|
-
else
|
|
45
|
-
lineQueue.push(line);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
return rl;
|
|
49
|
-
}
|
|
50
|
-
function askLine(promptStr) {
|
|
51
|
-
ensureRl();
|
|
52
|
-
process.stdout.write(promptStr);
|
|
53
|
-
const queued = lineQueue.shift();
|
|
54
|
-
if (queued !== undefined)
|
|
55
|
-
return Promise.resolve(queued);
|
|
56
|
-
return new Promise((resolve) => waiters.push(resolve));
|
|
57
|
-
}
|
|
58
|
-
function closeRl() {
|
|
59
|
-
rl?.close();
|
|
60
|
-
rl = null;
|
|
61
|
-
}
|
|
62
|
-
async function confirm(question, options, defaultYes) {
|
|
63
|
-
if (options.yes)
|
|
64
|
-
return defaultYes;
|
|
65
|
-
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
66
|
-
const answer = (await askLine(`${question} ${suffix} `)).trim().toLowerCase();
|
|
67
|
-
if (!answer)
|
|
68
|
-
return defaultYes;
|
|
69
|
-
return answer === "y" || answer === "yes";
|
|
70
|
-
}
|
|
71
|
-
async function promptText(question, defaultValue, options) {
|
|
72
|
-
if (options.yes)
|
|
73
|
-
return defaultValue;
|
|
74
|
-
const answer = (await askLine(`${question} [${defaultValue}]: `)).trim();
|
|
75
|
-
return answer || defaultValue;
|
|
76
|
-
}
|
|
77
|
-
function shellQuote(value) {
|
|
78
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
79
|
-
}
|
|
80
21
|
async function setupClaudeSettings(root, projectIdOverride, options) {
|
|
81
22
|
const settingsPath = join(root, ".claude", "settings.json");
|
|
82
23
|
let settings = {};
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Shared interactive-prompt plumbing for Pattern's CLI wizards
|
|
2
|
+
// (`pattern-check-gate init`, `pattern-mcp init`). Extracted from
|
|
3
|
+
// init-enforcement.ts so both wizards share one readline instance and one
|
|
4
|
+
// fix for the same bug, instead of drifting apart.
|
|
5
|
+
//
|
|
6
|
+
// A queue-based prompt helper, not readline/promises' question() --
|
|
7
|
+
// question() only starts listening for a line *after* it's called, but
|
|
8
|
+
// with piped/non-TTY stdin (as in an automated test, or `init | cat`)
|
|
9
|
+
// every line arrives in one synchronous burst, ahead of any await
|
|
10
|
+
// cycle. Confirmed directly: two sequential `rl.question()` calls on a
|
|
11
|
+
// piped `printf 'a\nb\n'` answer only the first and hang forever on the
|
|
12
|
+
// second -- Node even logs "Detected unsettled top-level await" in that
|
|
13
|
+
// repro. The fix is a small always-listening queue: a persistent 'line'
|
|
14
|
+
// listener buffers answers that arrive before they're asked for, so
|
|
15
|
+
// `askLine` either drains an already-buffered answer immediately or
|
|
16
|
+
// waits for the next 'line' event, whichever comes first -- correct for
|
|
17
|
+
// both a real interactive TTY (waiter path) and piped/scripted input
|
|
18
|
+
// (queue path).
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
let rl = null;
|
|
21
|
+
const lineQueue = [];
|
|
22
|
+
const waiters = [];
|
|
23
|
+
function ensureRl() {
|
|
24
|
+
if (!rl) {
|
|
25
|
+
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
rl.on("line", (line) => {
|
|
27
|
+
const waiter = waiters.shift();
|
|
28
|
+
if (waiter)
|
|
29
|
+
waiter(line);
|
|
30
|
+
else
|
|
31
|
+
lineQueue.push(line);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return rl;
|
|
35
|
+
}
|
|
36
|
+
export function askLine(promptStr) {
|
|
37
|
+
ensureRl();
|
|
38
|
+
process.stdout.write(promptStr);
|
|
39
|
+
const queued = lineQueue.shift();
|
|
40
|
+
if (queued !== undefined)
|
|
41
|
+
return Promise.resolve(queued);
|
|
42
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
43
|
+
}
|
|
44
|
+
export function closeRl() {
|
|
45
|
+
rl?.close();
|
|
46
|
+
rl = null;
|
|
47
|
+
}
|
|
48
|
+
export async function confirm(question, options, defaultYes) {
|
|
49
|
+
if (options.yes)
|
|
50
|
+
return defaultYes;
|
|
51
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
52
|
+
const answer = (await askLine(`${question} ${suffix} `)).trim().toLowerCase();
|
|
53
|
+
if (!answer)
|
|
54
|
+
return defaultYes;
|
|
55
|
+
return answer === "y" || answer === "yes";
|
|
56
|
+
}
|
|
57
|
+
export async function promptText(question, defaultValue, options) {
|
|
58
|
+
if (options.yes)
|
|
59
|
+
return defaultValue;
|
|
60
|
+
const answer = (await askLine(`${question} [${defaultValue}]: `)).trim();
|
|
61
|
+
return answer || defaultValue;
|
|
62
|
+
}
|
|
63
|
+
export function shellQuote(value) {
|
|
64
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
65
|
+
}
|
package/dist/telemetry.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
* install ID (see installId() below); a one-way SHA-256 hash of
|
|
19
19
|
* project_id, truncated to 16 hex chars -- never the raw project_id
|
|
20
20
|
* string; the verdict shape already written to the local call log
|
|
21
|
-
* (verdict, confidence, ensemble_triggered, estimated cost);
|
|
21
|
+
* (verdict, confidence, ensemble_triggered, estimated cost); on a
|
|
22
22
|
* failed Anthropic API call, only the HTTP status and a coarse error
|
|
23
23
|
* classification (rate_limit / insufficient_credit / other) -- never
|
|
24
|
-
* the request or response body
|
|
24
|
+
* the request or response body; and, on every invocation of the
|
|
25
|
+
* binary, a single `pattern_cli_started` event carrying only which
|
|
26
|
+
* mode it ran in (`server` or `init`) -- see captureCliStarted below.
|
|
25
27
|
* 2. `@posthog/mcp`'s standard MCP instrumentation, wired up in index.ts:
|
|
26
28
|
* tool name, call duration, error/success, and the same anonymous
|
|
27
29
|
* install ID as the distinct_id (via its `identify` option), so both
|
|
@@ -83,10 +85,12 @@ export function printTelemetryNoticeOnce() {
|
|
|
83
85
|
"When on, Pattern sends an anonymous per-install ID, a one-way hash",
|
|
84
86
|
"of project_id (never the raw string), the same verdict summary",
|
|
85
87
|
"already written to ~/.pattern/calls.log (verdict, confidence,",
|
|
86
|
-
"reason, estimated cost),
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
88
|
+
"reason, estimated cost), a single startup event noting whether this",
|
|
89
|
+
"run is the server or the `init` wizard, and standard MCP tool-call",
|
|
90
|
+
"analytics (tool name, duration, success/failure) via PostHog's MCP",
|
|
91
|
+
"SDK. Tool call arguments and responses are stripped before sending",
|
|
92
|
+
"-- component_need, domain, framework, existing_stack, and your API",
|
|
93
|
+
"key are never sent.",
|
|
90
94
|
"Full field list: https://github.com/donaldrichard19-LVD/pattern-mcp#telemetry",
|
|
91
95
|
"",
|
|
92
96
|
"To opt out: PATTERN_TELEMETRY=0",
|
|
@@ -216,6 +220,21 @@ export function captureRecommendation(args) {
|
|
|
216
220
|
served_from_ledger: args.servedFromLedger ?? false,
|
|
217
221
|
});
|
|
218
222
|
}
|
|
223
|
+
// Fires once per process invocation, immediately at startup, before the
|
|
224
|
+
// stdio transport connects and before either first-run notice prints.
|
|
225
|
+
// Distinct from @posthog/mcp's $mcp_initialize (which only fires once a
|
|
226
|
+
// real MCP client completes the JSON-RPC handshake): this fires for
|
|
227
|
+
// every real execution of the binary, including a bare `npx pattern-mcp`
|
|
228
|
+
// run in a terminal that never gets wired into a client, and the `init`
|
|
229
|
+
// subcommand. Exists to measure the gap between "npm registered a
|
|
230
|
+
// download" (includes scanner/mirror traffic that never runs the code at
|
|
231
|
+
// all) and "someone actually ran this" -- see
|
|
232
|
+
// project_pattern_reddit_launch_spike memory for why that gap mattered:
|
|
233
|
+
// a 2026-09-11 download spike showed almost no matching $mcp_initialize
|
|
234
|
+
// growth, and there was no signal at all for the step in between.
|
|
235
|
+
export function captureCliStarted(mode) {
|
|
236
|
+
capture("pattern_cli_started", { mode });
|
|
237
|
+
}
|
|
219
238
|
export function captureApiError(args) {
|
|
220
239
|
const { type, status } = classifyApiError(args.message);
|
|
221
240
|
capture("pattern_cli_api_error", {
|
|
@@ -225,13 +244,27 @@ export function captureApiError(args) {
|
|
|
225
244
|
project_hash: args.projectId ? hashProjectId(args.projectId) : null,
|
|
226
245
|
});
|
|
227
246
|
}
|
|
247
|
+
// How long shutdown will wait for a final flush before giving up.
|
|
248
|
+
// Verified directly (not assumed): with an unreachable PostHog host,
|
|
249
|
+
// posthog-node's client.shutdown() does not reject or time out on its
|
|
250
|
+
// own -- it hung well past 8s in a real test (an unroutable/blocked port
|
|
251
|
+
// looks like a stalled TCP connect, not an instant refusal). Without this
|
|
252
|
+
// race, every caller of shutdownTelemetry -- the SIGINT/SIGTERM handlers
|
|
253
|
+
// below AND the `init` wizard's normal exit path -- would hang
|
|
254
|
+
// indefinitely on a restricted network instead of exiting, directly
|
|
255
|
+
// contradicting this function's own "best-effort, never blocks" purpose.
|
|
256
|
+
const SHUTDOWN_TIMEOUT_MS = 2000;
|
|
228
257
|
// Best-effort drain on clean shutdown so the last event(s) of a session
|
|
229
258
|
// aren't dropped. Safe to call even when telemetry was never enabled.
|
|
259
|
+
// Always resolves within SHUTDOWN_TIMEOUT_MS regardless of network state.
|
|
230
260
|
export async function shutdownTelemetry() {
|
|
231
261
|
if (!client)
|
|
232
262
|
return;
|
|
233
263
|
try {
|
|
234
|
-
await
|
|
264
|
+
await Promise.race([
|
|
265
|
+
client.shutdown(),
|
|
266
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS).unref()),
|
|
267
|
+
]);
|
|
235
268
|
}
|
|
236
269
|
catch {
|
|
237
270
|
// Ignore -- process is exiting either way.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pattern-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "MCP server that turns your design guidance into a checkable process -- evaluates UI components from external libraries (shadcn/ui, 21st.dev, ReUI) or your own registered design system against a requirements checklist, then tells the agent whether to reuse an existing component or build one from a concrete design reference.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|