memoir-cli 3.8.0 → 3.8.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.
package/bin/memoir.js CHANGED
@@ -35,6 +35,7 @@ import { autopushCommand } from '../src/commands/autopush.js';
35
35
  import { whyCommand } from '../src/commands/why.js';
36
36
  import { autoRefreshCommand } from '../src/commands/auto-refresh.js';
37
37
  import { hooksInstallCommand, hooksUninstallCommand, hooksStatusCommand } from '../src/commands/hooks.js';
38
+ import { capture as track, telemetryCommand } from '../src/telemetry.js';
38
39
  import { createRequire } from 'module';
39
40
 
40
41
  const require = createRequire(import.meta.url);
@@ -692,8 +693,23 @@ program
692
693
  await import('../src/mcp.js');
693
694
  });
694
695
 
695
- program.hook('postAction', async () => {
696
+ program
697
+ .command('telemetry [action]')
698
+ .description('Anonymous usage telemetry: `on`, `off`, or `status` (default)')
699
+ .action(async (action) => {
700
+ try {
701
+ await telemetryCommand(action || 'status');
702
+ } catch (err) {
703
+ console.error(chalk.red('\n✖ Error:'), err.message);
704
+ process.exit(1);
705
+ }
706
+ });
707
+
708
+ program.hook('postAction', async (thisCommand, actionCommand) => {
696
709
  await checkForUpdate();
710
+ // Anonymous, opt-out usage event. postAction already awaits a network call
711
+ // (checkForUpdate), so this adds no perceived latency; no-op without a key.
712
+ try { await track('cli_command', { command: actionCommand?.name?.() || 'unknown' }); } catch {}
697
713
  });
698
714
 
699
715
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
4
4
  "mcpName": "io.github.camgitt/memoir",
5
5
  "description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences — across tools and machines.",
6
6
  "main": "src/index.js",
package/src/mcp.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  import { renderSession } from './session/render.js';
28
28
  import { injectInto, detectAvailableTargets } from './session/inject.js';
29
29
  import { findDecisions } from './commands/why.js';
30
+ import { capture as track } from './telemetry.js';
30
31
 
31
32
  const home = os.homedir();
32
33
 
@@ -171,6 +172,24 @@ const server = new McpServer({
171
172
  }
172
173
  });
173
174
 
175
+ // ── Anonymous telemetry (activation signal) ───────────────────────────────────
176
+ // Wrap server.tool ONCE so every registered handler emits an anonymous, no-PII
177
+ // "mcp_tool_used" event on call — the only place that proves memory was actually
178
+ // used (the North Star's activation event). Fire-and-forget; can't block or
179
+ // break a tool response. No-op unless a telemetry key is configured.
180
+ track('mcp_server_start');
181
+ const _registerTool = server.tool.bind(server);
182
+ server.tool = (name, ...rest) => {
183
+ const handler = rest[rest.length - 1];
184
+ if (typeof handler === 'function') {
185
+ rest[rest.length - 1] = (...args) => {
186
+ try { track('mcp_tool_used', { tool: name }); } catch {}
187
+ return handler(...args);
188
+ };
189
+ }
190
+ return _registerTool(name, ...rest);
191
+ };
192
+
174
193
  // ── Tools ────────────────────────────────────────────────────────────────────
175
194
 
176
195
  server.tool(
@@ -0,0 +1,108 @@
1
+ // Anonymous, opt-out usage telemetry.
2
+ //
3
+ // Fire-and-forget POST to PostHog's capture endpoint — no SDK, no batching/flush
4
+ // problem for a short-lived CLI, and a HARD NO-OP unless a project key is set.
5
+ // Honors DO_NOT_TRACK, CI, and `memoir telemetry off`. We NEVER send PII or any
6
+ // memory contents — only an anonymous install UUID, the event name, the OS, and
7
+ // the CLI version. All output goes to stderr so it can never corrupt the MCP
8
+ // stdio protocol (which speaks JSON-RPC over stdout).
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+ import os from 'os';
12
+ import { randomUUID } from 'crypto';
13
+ import { createRequire } from 'module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const VERSION = (() => {
17
+ try { return require('../package.json').version; } catch { return 'unknown'; }
18
+ })();
19
+
20
+ // PostHog PROJECT API key (phc_…). This is a PUBLIC client key — safe to ship in
21
+ // the package, same model as posthog-js in a web app. Set MEMOIR_POSTHOG_KEY or
22
+ // paste the project key here. Empty → telemetry is a silent no-op.
23
+ const POSTHOG_KEY = process.env.MEMOIR_POSTHOG_KEY || 'phc_vS7ZKfmZAcGnaCE7Zt4hvwFioJBs6jr8gutyapDpqFXW';
24
+ const POSTHOG_HOST = process.env.MEMOIR_POSTHOG_HOST || 'https://us.i.posthog.com';
25
+
26
+ const CONFIG_DIR = process.platform === 'win32'
27
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir')
28
+ : path.join(os.homedir(), '.config', 'memoir');
29
+ const ID_FILE = path.join(CONFIG_DIR, 'telemetry-id');
30
+ const OPTOUT_FILE = path.join(CONFIG_DIR, 'telemetry-off');
31
+ const DISCLOSED_FILE = path.join(CONFIG_DIR, 'telemetry-disclosed');
32
+
33
+ export function isEnabled() {
34
+ if (!POSTHOG_KEY) return false; // no key → no-op
35
+ if (['1', 'true'].includes(process.env.DO_NOT_TRACK)) return false;
36
+ if (process.env.CI) return false; // never track CI
37
+ if (['0', 'off', 'false'].includes(process.env.MEMOIR_TELEMETRY)) return false;
38
+ try { if (fs.existsSync(OPTOUT_FILE)) return false; } catch {}
39
+ return true;
40
+ }
41
+
42
+ function getInstallId() {
43
+ try {
44
+ if (fs.existsSync(ID_FILE)) return fs.readFileSync(ID_FILE, 'utf8').trim();
45
+ } catch {}
46
+ const id = randomUUID();
47
+ try { fs.ensureDirSync(CONFIG_DIR); fs.writeFileSync(ID_FILE, id); } catch {}
48
+ return id;
49
+ }
50
+
51
+ function discloseOnce() {
52
+ try {
53
+ if (fs.existsSync(DISCLOSED_FILE)) return;
54
+ fs.ensureDirSync(CONFIG_DIR);
55
+ fs.writeFileSync(DISCLOSED_FILE, new Date().toISOString());
56
+ process.stderr.write(
57
+ '\n memoir collects anonymous, no-PII usage stats to improve the tool.\n' +
58
+ ' Opt out anytime: `memoir telemetry off` (or set DO_NOT_TRACK=1).\n\n'
59
+ );
60
+ } catch {}
61
+ }
62
+
63
+ // Fire-and-forget. Never throws, never blocks beyond a short timeout, never
64
+ // touches stdout. Callers may await (CLI) or not (MCP) — both are safe.
65
+ export async function capture(event, properties = {}) {
66
+ try {
67
+ if (!isEnabled()) return;
68
+ discloseOnce();
69
+ const ctrl = new AbortController();
70
+ const timer = setTimeout(() => ctrl.abort(), 1500);
71
+ await fetch(`${POSTHOG_HOST}/capture/`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({
75
+ api_key: POSTHOG_KEY,
76
+ event,
77
+ distinct_id: getInstallId(),
78
+ properties: { ...properties, os: process.platform, node: process.version, version: VERSION, $lib: 'memoir-cli' },
79
+ timestamp: new Date().toISOString(),
80
+ }),
81
+ signal: ctrl.signal,
82
+ }).catch(() => {});
83
+ clearTimeout(timer);
84
+ } catch {
85
+ // Telemetry must never break a command or a tool call.
86
+ }
87
+ }
88
+
89
+ // `memoir telemetry on|off|status`
90
+ export async function telemetryCommand(action = 'status') {
91
+ const a = String(action).toLowerCase();
92
+ if (a === 'off') {
93
+ try { await fs.ensureDir(CONFIG_DIR); await fs.writeFile(OPTOUT_FILE, '1'); } catch {}
94
+ console.log(' Telemetry disabled. memoir will not send any usage events.');
95
+ return;
96
+ }
97
+ if (a === 'on') {
98
+ try { await fs.remove(OPTOUT_FILE); } catch {}
99
+ console.log(' Telemetry enabled (anonymous, no PII).');
100
+ return;
101
+ }
102
+ // status
103
+ const reason = !POSTHOG_KEY ? 'no project key configured'
104
+ : ['1', 'true'].includes(process.env.DO_NOT_TRACK) ? 'DO_NOT_TRACK is set'
105
+ : process.env.CI ? 'running in CI'
106
+ : (() => { try { return fs.existsSync(OPTOUT_FILE) ? 'opted out (`memoir telemetry off`)' : null; } catch { return null; } })();
107
+ console.log(reason ? ` Telemetry: OFF — ${reason}.` : ' Telemetry: ON — anonymous usage events (no PII). Disable with `memoir telemetry off`.');
108
+ }