memoir-cli
Advanced tools
+108
| // Anonymous, opt-out usage telemetry. | ||
| // | ||
| // Fire-and-forget POST to PostHog's capture endpoint — no SDK, no batching/flush | ||
| // problem for a short-lived CLI, and a HARD NO-OP unless a project key is set. | ||
| // Honors DO_NOT_TRACK, CI, and `memoir telemetry off`. We NEVER send PII or any | ||
| // memory contents — only an anonymous install UUID, the event name, the OS, and | ||
| // the CLI version. All output goes to stderr so it can never corrupt the MCP | ||
| // stdio protocol (which speaks JSON-RPC over stdout). | ||
| import fs from 'fs-extra'; | ||
| import path from 'path'; | ||
| import os from 'os'; | ||
| import { randomUUID } from 'crypto'; | ||
| import { createRequire } from 'module'; | ||
| const require = createRequire(import.meta.url); | ||
| const VERSION = (() => { | ||
| try { return require('../package.json').version; } catch { return 'unknown'; } | ||
| })(); | ||
| // PostHog PROJECT API key (phc_…). This is a PUBLIC client key — safe to ship in | ||
| // the package, same model as posthog-js in a web app. Set MEMOIR_POSTHOG_KEY or | ||
| // paste the project key here. Empty → telemetry is a silent no-op. | ||
| const POSTHOG_KEY = process.env.MEMOIR_POSTHOG_KEY || 'phc_vS7ZKfmZAcGnaCE7Zt4hvwFioJBs6jr8gutyapDpqFXW'; | ||
| const POSTHOG_HOST = process.env.MEMOIR_POSTHOG_HOST || 'https://us.i.posthog.com'; | ||
| const CONFIG_DIR = process.platform === 'win32' | ||
| ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir') | ||
| : path.join(os.homedir(), '.config', 'memoir'); | ||
| const ID_FILE = path.join(CONFIG_DIR, 'telemetry-id'); | ||
| const OPTOUT_FILE = path.join(CONFIG_DIR, 'telemetry-off'); | ||
| const DISCLOSED_FILE = path.join(CONFIG_DIR, 'telemetry-disclosed'); | ||
| export function isEnabled() { | ||
| if (!POSTHOG_KEY) return false; // no key → no-op | ||
| if (['1', 'true'].includes(process.env.DO_NOT_TRACK)) return false; | ||
| if (process.env.CI) return false; // never track CI | ||
| if (['0', 'off', 'false'].includes(process.env.MEMOIR_TELEMETRY)) return false; | ||
| try { if (fs.existsSync(OPTOUT_FILE)) return false; } catch {} | ||
| return true; | ||
| } | ||
| function getInstallId() { | ||
| try { | ||
| if (fs.existsSync(ID_FILE)) return fs.readFileSync(ID_FILE, 'utf8').trim(); | ||
| } catch {} | ||
| const id = randomUUID(); | ||
| try { fs.ensureDirSync(CONFIG_DIR); fs.writeFileSync(ID_FILE, id); } catch {} | ||
| return id; | ||
| } | ||
| function discloseOnce() { | ||
| try { | ||
| if (fs.existsSync(DISCLOSED_FILE)) return; | ||
| fs.ensureDirSync(CONFIG_DIR); | ||
| fs.writeFileSync(DISCLOSED_FILE, new Date().toISOString()); | ||
| process.stderr.write( | ||
| '\n memoir collects anonymous, no-PII usage stats to improve the tool.\n' + | ||
| ' Opt out anytime: `memoir telemetry off` (or set DO_NOT_TRACK=1).\n\n' | ||
| ); | ||
| } catch {} | ||
| } | ||
| // Fire-and-forget. Never throws, never blocks beyond a short timeout, never | ||
| // touches stdout. Callers may await (CLI) or not (MCP) — both are safe. | ||
| export async function capture(event, properties = {}) { | ||
| try { | ||
| if (!isEnabled()) return; | ||
| discloseOnce(); | ||
| const ctrl = new AbortController(); | ||
| const timer = setTimeout(() => ctrl.abort(), 1500); | ||
| await fetch(`${POSTHOG_HOST}/capture/`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| api_key: POSTHOG_KEY, | ||
| event, | ||
| distinct_id: getInstallId(), | ||
| properties: { ...properties, os: process.platform, node: process.version, version: VERSION, $lib: 'memoir-cli' }, | ||
| timestamp: new Date().toISOString(), | ||
| }), | ||
| signal: ctrl.signal, | ||
| }).catch(() => {}); | ||
| clearTimeout(timer); | ||
| } catch { | ||
| // Telemetry must never break a command or a tool call. | ||
| } | ||
| } | ||
| // `memoir telemetry on|off|status` | ||
| export async function telemetryCommand(action = 'status') { | ||
| const a = String(action).toLowerCase(); | ||
| if (a === 'off') { | ||
| try { await fs.ensureDir(CONFIG_DIR); await fs.writeFile(OPTOUT_FILE, '1'); } catch {} | ||
| console.log(' Telemetry disabled. memoir will not send any usage events.'); | ||
| return; | ||
| } | ||
| if (a === 'on') { | ||
| try { await fs.remove(OPTOUT_FILE); } catch {} | ||
| console.log(' Telemetry enabled (anonymous, no PII).'); | ||
| return; | ||
| } | ||
| // status | ||
| const reason = !POSTHOG_KEY ? 'no project key configured' | ||
| : ['1', 'true'].includes(process.env.DO_NOT_TRACK) ? 'DO_NOT_TRACK is set' | ||
| : process.env.CI ? 'running in CI' | ||
| : (() => { try { return fs.existsSync(OPTOUT_FILE) ? 'opted out (`memoir telemetry off`)' : null; } catch { return null; } })(); | ||
| console.log(reason ? ` Telemetry: OFF — ${reason}.` : ' Telemetry: ON — anonymous usage events (no PII). Disable with `memoir telemetry off`.'); | ||
| } |
+17
-1
@@ -38,2 +38,3 @@ #!/usr/bin/env node | ||
| import { hooksInstallCommand, hooksUninstallCommand, hooksStatusCommand } from '../src/commands/hooks.js'; | ||
| import { capture as track, telemetryCommand } from '../src/telemetry.js'; | ||
| import { createRequire } from 'module'; | ||
@@ -696,6 +697,21 @@ | ||
| program.hook('postAction', async () => { | ||
| program | ||
| .command('telemetry [action]') | ||
| .description('Anonymous usage telemetry: `on`, `off`, or `status` (default)') | ||
| .action(async (action) => { | ||
| try { | ||
| await telemetryCommand(action || 'status'); | ||
| } catch (err) { | ||
| console.error(chalk.red('\n✖ Error:'), err.message); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.hook('postAction', async (thisCommand, actionCommand) => { | ||
| await checkForUpdate(); | ||
| // Anonymous, opt-out usage event. postAction already awaits a network call | ||
| // (checkForUpdate), so this adds no perceived latency; no-op without a key. | ||
| try { await track('cli_command', { command: actionCommand?.name?.() || 'unknown' }); } catch {} | ||
| }); | ||
| program.parse(); |
+1
-1
| { | ||
| "name": "memoir-cli", | ||
| "version": "3.8.0", | ||
| "version": "3.8.1", | ||
| "mcpName": "io.github.camgitt/memoir", | ||
@@ -5,0 +5,0 @@ "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.", |
+19
-0
@@ -30,2 +30,3 @@ #!/usr/bin/env node | ||
| import { findDecisions } from './commands/why.js'; | ||
| import { capture as track } from './telemetry.js'; | ||
@@ -175,2 +176,20 @@ const home = os.homedir(); | ||
| // ── Anonymous telemetry (activation signal) ─────────────────────────────────── | ||
| // Wrap server.tool ONCE so every registered handler emits an anonymous, no-PII | ||
| // "mcp_tool_used" event on call — the only place that proves memory was actually | ||
| // used (the North Star's activation event). Fire-and-forget; can't block or | ||
| // break a tool response. No-op unless a telemetry key is configured. | ||
| track('mcp_server_start'); | ||
| const _registerTool = server.tool.bind(server); | ||
| server.tool = (name, ...rest) => { | ||
| const handler = rest[rest.length - 1]; | ||
| if (typeof handler === 'function') { | ||
| rest[rest.length - 1] = (...args) => { | ||
| try { track('mcp_tool_used', { tool: name }); } catch {} | ||
| return handler(...args); | ||
| }; | ||
| } | ||
| return _registerTool(name, ...rest); | ||
| }; | ||
| // ── Tools ──────────────────────────────────────────────────────────────────── | ||
@@ -177,0 +196,0 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
376834
1.67%60
1.69%9122
1.48%44
29.41%38
2.7%