memoir-cli 3.7.1 → 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/README.md +10 -3
- package/bin/memoir.js +18 -1
- package/package.json +2 -2
- package/src/commands/doctor.js +4 -14
- package/src/commands/push.js +87 -0
- package/src/mcp.js +19 -0
- package/src/telemetry.js +108 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
npx memoir-cli
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and
|
|
18
|
+
One command. No install, no config, no API keys. Your AI now has persistent memory across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, and 6 more tools.
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -44,7 +44,7 @@ npx memoir-cli
|
|
|
44
44
|
|
|
45
45
|
That's it. memoir detects your AI tools, configures MCP, and activates memory. No global install needed.
|
|
46
46
|
|
|
47
|
-
Your AI gets
|
|
47
|
+
Your AI gets 14 memory tools:
|
|
48
48
|
|
|
49
49
|
| MCP Tool | What it does |
|
|
50
50
|
|----------|-------------|
|
|
@@ -55,6 +55,13 @@ Your AI gets 7 memory tools:
|
|
|
55
55
|
| `memoir_consolidate` | Analyze memories for duplicates, staleness, and bloat |
|
|
56
56
|
| `memoir_status` | See which AI tools are detected |
|
|
57
57
|
| `memoir_profiles` | Switch between work/personal |
|
|
58
|
+
| `memoir_set_goal` | Set the current session goal (pinned into CLAUDE.md) |
|
|
59
|
+
| `memoir_add_next` | Add a next action to the current session |
|
|
60
|
+
| `memoir_complete_next` | Mark a next action as done |
|
|
61
|
+
| `memoir_note` | Record a decision with its rationale |
|
|
62
|
+
| `memoir_ask` | Capture an open question for later |
|
|
63
|
+
| `memoir_session` | Show goals, next actions, decisions, and recent sessions |
|
|
64
|
+
| `memoir_why` | Look up why a past decision was made |
|
|
58
65
|
|
|
59
66
|
## Why memoir
|
|
60
67
|
|
|
@@ -62,7 +69,7 @@ Your AI forgets everything between sessions. You re-explain your codebase, your
|
|
|
62
69
|
|
|
63
70
|
memoir fixes this by giving your AI a shared memory layer that works across **every tool you use**. Tell Claude something once. Cursor knows it too. Sync AI memory between tools, back it up to the cloud, restore it on any machine. And when your memories pile up, `memoir consolidate` cleans house — finds duplicates, flags stale context, and optionally uses AI to merge and prune.
|
|
64
71
|
|
|
65
|
-
**
|
|
72
|
+
**11 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev.
|
|
66
73
|
|
|
67
74
|
## Sync across machines
|
|
68
75
|
|
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);
|
|
@@ -112,6 +113,7 @@ program
|
|
|
112
113
|
.description('Back up your AI memory to the cloud')
|
|
113
114
|
.option('--only <tools>', 'Only sync specific tools (comma-separated)')
|
|
114
115
|
.option('-p, --profile <name>', 'Use a specific profile')
|
|
116
|
+
.option('--redact', 'Strip detected secrets from synced files before they are backed up')
|
|
115
117
|
.action(async (options) => {
|
|
116
118
|
try {
|
|
117
119
|
await pushCommand(options);
|
|
@@ -691,8 +693,23 @@ program
|
|
|
691
693
|
await import('../src/mcp.js');
|
|
692
694
|
});
|
|
693
695
|
|
|
694
|
-
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) => {
|
|
695
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 {}
|
|
696
713
|
});
|
|
697
714
|
|
|
698
715
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/camgitt/memoir.git"
|
|
23
|
+
"url": "git+https://github.com/camgitt/memoir.git"
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://memoir.sh",
|
|
26
26
|
"bugs": {
|
package/src/commands/doctor.js
CHANGED
|
@@ -8,15 +8,7 @@ import os from 'os';
|
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { getConfig } from '../config.js';
|
|
10
10
|
import { adapters } from '../adapters/index.js';
|
|
11
|
-
|
|
12
|
-
const SECRET_PATTERNS = [
|
|
13
|
-
{ pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI/Stripe secret key' },
|
|
14
|
-
{ pattern: /key-[a-zA-Z0-9]{20,}/, label: 'API key' },
|
|
15
|
-
{ pattern: /ghp_[a-zA-Z0-9]{36,}/, label: 'GitHub personal access token' },
|
|
16
|
-
{ pattern: /gho_[a-zA-Z0-9]{36,}/, label: 'GitHub OAuth token' },
|
|
17
|
-
{ pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' },
|
|
18
|
-
{ pattern: /Bearer\s+[a-zA-Z0-9._\-]{20,}/, label: 'Bearer token' },
|
|
19
|
-
];
|
|
11
|
+
import { scanForSecrets as scanTextForSecrets } from '../security/scanner.js';
|
|
20
12
|
|
|
21
13
|
const SENSITIVE_FILENAMES = ['.env', 'credentials', 'token.json'];
|
|
22
14
|
|
|
@@ -61,11 +53,9 @@ async function scanForSecrets(files) {
|
|
|
61
53
|
// Skip files larger than 1MB
|
|
62
54
|
if (stat.size > 1024 * 1024) continue;
|
|
63
55
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
56
|
+
const { found } = scanTextForSecrets(content);
|
|
57
|
+
if (found.length > 0) {
|
|
58
|
+
warnings.push({ file: filePath, reason: found[0].label });
|
|
69
59
|
}
|
|
70
60
|
} catch {
|
|
71
61
|
// Skip unreadable files
|
package/src/commands/push.js
CHANGED
|
@@ -19,6 +19,52 @@ import { paths as sessionPaths, readSession, addNote, recordSessionEnd } from '.
|
|
|
19
19
|
import { renderSession } from '../session/render.js';
|
|
20
20
|
import { injectInto, detectAvailableTargets } from '../session/inject.js';
|
|
21
21
|
|
|
22
|
+
// Recursively scan every staged file (the REAL tool memory/config files about
|
|
23
|
+
// to be uploaded — CLAUDE.md, .cursorrules, settings.json, project configs,
|
|
24
|
+
// etc.) for secrets. When `redact` is true, rewrite each offending file in
|
|
25
|
+
// place so the cleaned version is what gets uploaded (and encrypted, if on).
|
|
26
|
+
// Returns { findings, scanned } where findings is a flat list of detections
|
|
27
|
+
// keyed by file. Best-effort: unreadable/binary files are skipped.
|
|
28
|
+
export async function scanStagedFiles(dir, { redact = false } = {}) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
let scanned = 0;
|
|
31
|
+
|
|
32
|
+
const walk = async (d) => {
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await fs.readdir(d, { withFileTypes: true });
|
|
36
|
+
} catch { return; }
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const fullPath = path.join(d, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
await walk(fullPath);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const stat = await fs.stat(fullPath);
|
|
45
|
+
// Skip files larger than 1MB — same threshold as doctor's scan
|
|
46
|
+
if (stat.size > 1024 * 1024) continue;
|
|
47
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
48
|
+
scanned++;
|
|
49
|
+
const { found, clean } = scanForSecrets(content);
|
|
50
|
+
if (found.length > 0) {
|
|
51
|
+
for (const f of found) {
|
|
52
|
+
findings.push({ file: fullPath, label: f.label, redacted: f.redacted });
|
|
53
|
+
}
|
|
54
|
+
if (redact && clean !== content) {
|
|
55
|
+
await fs.writeFile(fullPath, clean);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Skip unreadable / non-text files
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await walk(dir);
|
|
65
|
+
return { findings, scanned };
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
export async function pushCommand(options = {}) {
|
|
23
69
|
let config = await getConfig(options.profile);
|
|
24
70
|
|
|
@@ -205,6 +251,47 @@ export async function pushCommand(options = {}) {
|
|
|
205
251
|
}
|
|
206
252
|
}
|
|
207
253
|
|
|
254
|
+
// Scan the REAL files being synced (the staged tool memory/config files,
|
|
255
|
+
// not just the handoff blob) for secrets before they leave the machine.
|
|
256
|
+
// • --redact → strip secrets in place, then upload (sanitized)
|
|
257
|
+
// • otherwise → WARN and continue
|
|
258
|
+
// • background autopush → stay silent and continue
|
|
259
|
+
// We deliberately do NOT hard-block. This is a zero-knowledge encrypted
|
|
260
|
+
// backup of the user's OWN files; silently refusing to back up — which the
|
|
261
|
+
// detached `autopush` Stop-hook path (stdio:'ignore', MEMOIR_AUTOPUSH=1, no
|
|
262
|
+
// TTY) would hit on any false-positive match — is a worse failure than
|
|
263
|
+
// backing up. A future `--strict` flag could fail-closed for the
|
|
264
|
+
// encrypt-off / shared-destination case. Wrapped so a scanner error can
|
|
265
|
+
// never break the push.
|
|
266
|
+
const background = process.env.MEMOIR_AUTOPUSH === '1';
|
|
267
|
+
try {
|
|
268
|
+
const { findings } = await scanStagedFiles(stagingDir, { redact: options.redact === true });
|
|
269
|
+
if (findings.length > 0) {
|
|
270
|
+
if (options.redact === true) {
|
|
271
|
+
spinner.stop();
|
|
272
|
+
console.log(chalk.yellow(`\n 🔒 Redacted ${findings.length} secret(s) from synced files before upload:`));
|
|
273
|
+
for (const f of findings.slice(0, 5)) {
|
|
274
|
+
console.log(chalk.gray(` ${path.basename(f.file)}: ${f.label} (${f.redacted})`));
|
|
275
|
+
}
|
|
276
|
+
if (findings.length > 5) console.log(chalk.gray(` ...and ${findings.length - 5} more`));
|
|
277
|
+
spinner.start();
|
|
278
|
+
} else if (!background) {
|
|
279
|
+
// Warn (interactive or piped) but never block — the backup proceeds.
|
|
280
|
+
spinner.stop();
|
|
281
|
+
console.log(chalk.yellow(`\n ⚠️ ${findings.length} potential secret(s) in synced files (backed up as-is):`));
|
|
282
|
+
for (const f of findings.slice(0, 5)) {
|
|
283
|
+
console.log(chalk.gray(` ${path.basename(f.file)}: ${f.label} (${f.redacted})`));
|
|
284
|
+
}
|
|
285
|
+
if (findings.length > 5) console.log(chalk.gray(` ...and ${findings.length - 5} more`));
|
|
286
|
+
console.log(chalk.gray(' Re-run with ') + chalk.cyan('--redact') + chalk.gray(' to strip them from the backup.'));
|
|
287
|
+
spinner.start();
|
|
288
|
+
}
|
|
289
|
+
// background autopush: silent, continue — never block the auto-backup
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Secret scan is best-effort — never let it break the push.
|
|
293
|
+
}
|
|
294
|
+
|
|
208
295
|
// Encrypt if enabled (or ask on first push if not configured)
|
|
209
296
|
let uploadDir = stagingDir;
|
|
210
297
|
let encrypted = false;
|
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
|
+
}
|