pattern-mcp 0.12.1 → 0.14.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 +303 -266
- package/dist/client-connect.js +265 -0
- package/dist/index.js +391 -245
- package/dist/init-enforcement.js +1 -60
- package/dist/prompt.js +65 -0
- package/dist/telemetry.js +63 -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
|
|
@@ -34,6 +36,15 @@
|
|
|
34
36
|
* itself are never sent, by either half. See SECURITY.md and README.md for
|
|
35
37
|
* the full disclosure and how to opt out.
|
|
36
38
|
*
|
|
39
|
+
* Manual/ad-hoc test sessions (a one-off MCP client run by hand while
|
|
40
|
+
* debugging, not a checked-in script) should set PATTERN_TELEMETRY=0 before
|
|
41
|
+
* connecting -- there's no way for this file to distinguish that from real
|
|
42
|
+
* usage on its own, and self-testing was previously showing up as if it
|
|
43
|
+
* were adoption (see project_pattern_activation_funnel memory: roughly half
|
|
44
|
+
* of all recorded handshakes turned out to be internal test/smoke-test
|
|
45
|
+
* clients). Checked-in test scripts (e.g. scripts/test-client.mjs) default
|
|
46
|
+
* this off already.
|
|
47
|
+
*
|
|
37
48
|
* Reuses Pattern's existing PostHog project (the same one the marketing
|
|
38
49
|
* site sends browser events to) with its public, write-only project key --
|
|
39
50
|
* safe to embed in a distributed package the same way that key is already
|
|
@@ -83,10 +94,12 @@ export function printTelemetryNoticeOnce() {
|
|
|
83
94
|
"When on, Pattern sends an anonymous per-install ID, a one-way hash",
|
|
84
95
|
"of project_id (never the raw string), the same verdict summary",
|
|
85
96
|
"already written to ~/.pattern/calls.log (verdict, confidence,",
|
|
86
|
-
"reason, estimated cost),
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
97
|
+
"reason, estimated cost), a single startup event noting whether this",
|
|
98
|
+
"run is the server or the `init` wizard, and standard MCP tool-call",
|
|
99
|
+
"analytics (tool name, duration, success/failure) via PostHog's MCP",
|
|
100
|
+
"SDK. Tool call arguments and responses are stripped before sending",
|
|
101
|
+
"-- component_need, domain, framework, existing_stack, and your API",
|
|
102
|
+
"key are never sent.",
|
|
90
103
|
"Full field list: https://github.com/donaldrichard19-LVD/pattern-mcp#telemetry",
|
|
91
104
|
"",
|
|
92
105
|
"To opt out: PATTERN_TELEMETRY=0",
|
|
@@ -216,6 +229,35 @@ export function captureRecommendation(args) {
|
|
|
216
229
|
served_from_ledger: args.servedFromLedger ?? false,
|
|
217
230
|
});
|
|
218
231
|
}
|
|
232
|
+
// Fires once per process invocation, immediately at startup, before the
|
|
233
|
+
// stdio transport connects and before either first-run notice prints.
|
|
234
|
+
// Distinct from @posthog/mcp's $mcp_initialize (which only fires once a
|
|
235
|
+
// real MCP client completes the JSON-RPC handshake): this fires for
|
|
236
|
+
// every real execution of the binary, including a bare `npx pattern-mcp`
|
|
237
|
+
// run in a terminal that never gets wired into a client, and the `init`
|
|
238
|
+
// subcommand. Exists to measure the gap between "npm registered a
|
|
239
|
+
// download" (includes scanner/mirror traffic that never runs the code at
|
|
240
|
+
// all) and "someone actually ran this" -- see
|
|
241
|
+
// project_pattern_reddit_launch_spike memory for why that gap mattered:
|
|
242
|
+
// a 2026-09-11 download spike showed almost no matching $mcp_initialize
|
|
243
|
+
// growth, and there was no signal at all for the step in between.
|
|
244
|
+
export function captureCliStarted(mode) {
|
|
245
|
+
capture("pattern_cli_started", { mode });
|
|
246
|
+
}
|
|
247
|
+
// Paired with captureCliStarted so a start with no matching handshake is
|
|
248
|
+
// diagnosable instead of silent -- added after a 2026-09-13 incident where
|
|
249
|
+
// 33 starts in one hour produced exactly 1 successful handshake, and
|
|
250
|
+
// telemetry had no way to say why the other 32 processes ended (see
|
|
251
|
+
// project_pattern_activation_funnel memory). Only a coarse reason and the
|
|
252
|
+
// thrown value's constructor name travel -- never the error message or
|
|
253
|
+
// stack, which could contain a file path, a stray argument value, or other
|
|
254
|
+
// local detail never sent by design (see this file's header).
|
|
255
|
+
export function captureCliExited(reason, err) {
|
|
256
|
+
capture("pattern_cli_exited", {
|
|
257
|
+
exit_reason: reason,
|
|
258
|
+
error_name: err instanceof Error ? err.name : null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
219
261
|
export function captureApiError(args) {
|
|
220
262
|
const { type, status } = classifyApiError(args.message);
|
|
221
263
|
capture("pattern_cli_api_error", {
|
|
@@ -225,13 +267,27 @@ export function captureApiError(args) {
|
|
|
225
267
|
project_hash: args.projectId ? hashProjectId(args.projectId) : null,
|
|
226
268
|
});
|
|
227
269
|
}
|
|
270
|
+
// How long shutdown will wait for a final flush before giving up.
|
|
271
|
+
// Verified directly (not assumed): with an unreachable PostHog host,
|
|
272
|
+
// posthog-node's client.shutdown() does not reject or time out on its
|
|
273
|
+
// own -- it hung well past 8s in a real test (an unroutable/blocked port
|
|
274
|
+
// looks like a stalled TCP connect, not an instant refusal). Without this
|
|
275
|
+
// race, every caller of shutdownTelemetry -- the SIGINT/SIGTERM handlers
|
|
276
|
+
// below AND the `init` wizard's normal exit path -- would hang
|
|
277
|
+
// indefinitely on a restricted network instead of exiting, directly
|
|
278
|
+
// contradicting this function's own "best-effort, never blocks" purpose.
|
|
279
|
+
const SHUTDOWN_TIMEOUT_MS = 2000;
|
|
228
280
|
// Best-effort drain on clean shutdown so the last event(s) of a session
|
|
229
281
|
// aren't dropped. Safe to call even when telemetry was never enabled.
|
|
282
|
+
// Always resolves within SHUTDOWN_TIMEOUT_MS regardless of network state.
|
|
230
283
|
export async function shutdownTelemetry() {
|
|
231
284
|
if (!client)
|
|
232
285
|
return;
|
|
233
286
|
try {
|
|
234
|
-
await
|
|
287
|
+
await Promise.race([
|
|
288
|
+
client.shutdown(),
|
|
289
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS).unref()),
|
|
290
|
+
]);
|
|
235
291
|
}
|
|
236
292
|
catch {
|
|
237
293
|
// 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.14.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",
|