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 +17 -1
- package/package.json +1 -1
- package/src/mcp.js +19 -0
- package/src/telemetry.js +108 -0
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
|
|
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.
|
|
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(
|
package/src/telemetry.js
ADDED
|
@@ -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
|
+
}
|