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.
@@ -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); and, on a
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), and standard MCP tool-call analytics (tool",
87
- "name, duration, success/failure) via PostHog's MCP SDK. Tool call",
88
- "arguments and responses are stripped before sending -- component_need,",
89
- "domain, framework, existing_stack, and your API key are never sent.",
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 client.shutdown();
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.12.1",
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",