@extenshi/mcp 0.5.0 → 0.5.1

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.
@@ -4,9 +4,10 @@
4
4
  * and @extenshi/mcp (it figures out which one it is from its own package.json).
5
5
  *
6
6
  * Fires a single `cli_installed` / `mcp_installed` event when the package is
7
- * installed as a real dependency, so we can see adoption in PostHog. It is a
8
- * standalone CommonJS script using ONLY Node built-ins (no posthog-node, no
9
- * build step) so it can run the instant npm extracts the tarball.
7
+ * installed as a real dependency ON A MACHINE THAT HAS ALREADY ACCEPTED
8
+ * telemetry, so we can see adoption in PostHog. It is a standalone CommonJS
9
+ * script using ONLY Node built-ins (no posthog-node, no build step) so it can
10
+ * run the instant npm extracts the tarball.
10
11
  *
11
12
  * Hard rules (a postinstall that breaks `npm install` is unacceptable):
12
13
  * - ALWAYS exits 0, even on any error — wrapped in try/catch + a hard timeout.
@@ -15,21 +16,58 @@
15
16
  * It self-skips (no event, no network) when:
16
17
  * - the package is NOT under node_modules — i.e. our own monorepo workspace
17
18
  * install (tools/<pkg>), so dev installs never inflate adoption counts;
18
- * - DO_NOT_TRACK is truthy, EXTENSHI_TELEMETRY is off, or config
19
- * ~/.extenshi/config.json has "telemetry": false;
19
+ * - ~/.extenshi/config.json does not hold "telemetry": true — consent must be
20
+ * RECORDED, not merely un-refused. A postinstall runs before the CLI can
21
+ * ask anything, so "no answer yet" is not permission (see the ACCEPTED-ONLY
22
+ * note below);
23
+ * - DO_NOT_TRACK is truthy or EXTENSHI_TELEMETRY is off;
20
24
  * - CI is set (CI re-installs would massively inflate the count);
21
25
  * - no ingestion key is configured.
22
26
  *
23
- * Privacy: sends only the package version, Node version, coarse os/arch, and an
24
- * anonymous per-install UUID (the same id the runtime telemetry uses). Never any
25
- * path, package contents, or user input.
27
+ * Privacy: sends only the package version, Node version, coarse os/arch, a
28
+ * first-install-on-this-machine boolean, and an anonymous per-install UUID (the
29
+ * same id the runtime telemetry uses). Never any path, package contents, or
30
+ * user input.
31
+ *
32
+ * ACCEPTED-ONLY, and what that costs. Until 2026-08-13 this fired unless the
33
+ * user had actively opted out, which meant a first-ever install reported before
34
+ * anyone had been asked — the one place consent did not reach after the CLI
35
+ * grew a first-run question (tools/extenshi-cli/src/telemetry-consent.ts). The
36
+ * gate is now positive, and the consequences are deliberate:
37
+ * - a FIRST install never pings. Only the CLI's first-run question writes
38
+ * `telemetry: true`, and that happens on the first RUN, so the earliest
39
+ * ping is the next upgrade of an accepted machine.
40
+ * - @extenshi/mcp is headless and never prompts, so it can only ping on a
41
+ * machine where the CLI recorded consent into the shared config. Treat its
42
+ * install count as gone, not as a number that dropped.
43
+ * - `first_extenshi_install` is therefore ~always false now. It is kept so the
44
+ * event schema does not change under existing PostHog queries, not because
45
+ * it still measures anything.
46
+ *
47
+ * NB for anyone reading this as an adoption metric: it counts npm EXTRACTIONS,
48
+ * not users. Most of them are ecosystem scanners that install a new version once
49
+ * on a throwaway machine and never run the binary. The "did a client actually
50
+ * launch the server" signal is `mcp_server_started`, emitted from
51
+ * @extenshi/mcp's src/startup.ts — a relative path would be wrong in one of the
52
+ * two packages, since this file is vendored byte-identical into both.
26
53
  */
27
54
 
28
55
  // Shared EU PostHog project key (114791) — public, write-only, already in our
29
56
  // web bundles. Override with EXTENSHI_POSTHOG_KEY; empty disables the ping.
57
+ // NB: this line's NUMBER is pinned in .infisicalignore
58
+ // (`…/postinstall.cjs:generic-api-key:<line>`) — gitleaks fingerprints include
59
+ // it, so ADDING OR REMOVING LINES ABOVE HERE breaks the allowlist and turns the
60
+ // key into a CI secret-scan failure. Move the fingerprint with it.
30
61
  const EMBEDDED_KEY = 'phc_fqKrAmtNZvJqe0krpYB3UwYqALLpT1WM8m5LtNs9eUu'
31
62
  const DEFAULT_HOST = 'https://eu.i.posthog.com'
32
63
 
64
+ /**
65
+ * Every guard below calls this as `return done()`, not as a bare statement. The
66
+ * process.exit is what actually stops the script, but the `return` is what keeps
67
+ * the early exit true if this ever stops exiting synchronously — without it, a
68
+ * deferred done() would let the code past the consent gate run on a machine that
69
+ * never agreed to be measured.
70
+ */
33
71
  function done() {
34
72
  process.exit(0)
35
73
  }
@@ -42,22 +80,22 @@ try {
42
80
  const { randomUUID } = require('node:crypto')
43
81
 
44
82
  // Real dependency install only — workspace path has no /node_modules/ segment.
45
- if (!__dirname.includes(`${path.sep}node_modules${path.sep}`)) done()
83
+ if (!__dirname.includes(`${path.sep}node_modules${path.sep}`)) return done()
46
84
 
47
85
  // CI re-installs would inflate the adoption count — skip them.
48
- if (process.env.CI) done()
86
+ if (process.env.CI) return done()
49
87
 
50
88
  const key = (process.env.EXTENSHI_POSTHOG_KEY || EMBEDDED_KEY).trim()
51
- if (!key) done()
89
+ if (!key) return done()
52
90
 
53
91
  const dnt = String(process.env.DO_NOT_TRACK || '')
54
92
  .trim()
55
93
  .toLowerCase()
56
- if (['1', 'true', 'yes', 'on'].includes(dnt)) done()
94
+ if (['1', 'true', 'yes', 'on'].includes(dnt)) return done()
57
95
  const optOut = String(process.env.EXTENSHI_TELEMETRY || '')
58
96
  .trim()
59
97
  .toLowerCase()
60
- if (['0', 'false', 'off', 'no'].includes(optOut)) done()
98
+ if (['0', 'false', 'off', 'no'].includes(optOut)) return done()
61
99
 
62
100
  // Anonymous per-install id, shared with the runtime telemetry module.
63
101
  const configPath = path.join(os.homedir(), '.extenshi', 'config.json')
@@ -65,8 +103,23 @@ try {
65
103
  try {
66
104
  cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'))
67
105
  } catch {}
68
- if (cfg && cfg.telemetry === false) done()
69
- let anonId = cfg && cfg.anonId
106
+
107
+ // Positive consent only. An install happens before the CLI can put the
108
+ // question, so an absent answer is "not asked yet", never "yes" — this exits
109
+ // for a machine that declined AND for one that has never been asked. Every
110
+ // line below this point, including minting the anonymous id, is reached only
111
+ // with a recorded `true`, which is what stops a bare `npx` from creating an
112
+ // identity on a machine that never agreed to one.
113
+ if (!cfg || cfg.telemetry !== true) return done()
114
+
115
+ let anonId = cfg.anonId
116
+ // Retained for event-schema stability, but it no longer measures anything:
117
+ // accepting the first-run question writes `telemetry: true` and then emits an
118
+ // event, which mints the id — so by the time this gate opens, an anonId is
119
+ // already there and this is false. It stays true only in the odd case of a
120
+ // config that kept the answer but lost the id. Before the accepted-only gate
121
+ // it was the new-machine-vs-upgrade discriminator described above.
122
+ const firstExtenshiInstall = !anonId
70
123
  if (!anonId) {
71
124
  anonId = randomUUID()
72
125
  try {
@@ -95,6 +148,7 @@ try {
95
148
  arch: process.arch,
96
149
  ci: false,
97
150
  source: 'npm_postinstall',
151
+ first_extenshi_install: firstExtenshiInstall,
98
152
  },
99
153
  })
100
154