memoir-cli 3.17.5 → 3.17.6
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 +2 -2
- package/bin/memoir.js +5 -5
- package/package.json +1 -1
- package/src/cloud/auth.js +33 -4
- package/src/commands/login.js +7 -5
- package/src/commands/share.js +11 -1
- package/src/commands/upgrade.js +8 -8
- package/src/mcp.js +2 -1
- package/src/telemetry.js +90 -28
- package/supabase/migrations/20261027193000_purge_expired_shares.sql +19 -0
- package/supabase/migrations/20261027200000_shared_links_expires_at_not_null.sql +19 -0
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ Local pushes lock the complete read/merge/write operation. Git rejects conflicti
|
|
|
113
113
|
|
|
114
114
|
### Cloud
|
|
115
115
|
|
|
116
|
-
New cloud writes require a user-held secret of at least 12 characters in `MEMOIR_CLOUD_PASSPHRASE` (or `MEMOIR_PASSPHRASE`). It is not derived from account identity or sent in metadata.
|
|
116
|
+
New cloud writes require a user-held secret of at least 12 characters in `MEMOIR_CLOUD_PASSPHRASE` (or `MEMOIR_PASSPHRASE`). It is not derived from account identity or sent in metadata. Client-side encryption is available on every plan; it is not Pro-only.
|
|
117
117
|
|
|
118
118
|
```sh
|
|
119
119
|
memoir cloud push
|
|
@@ -163,7 +163,7 @@ Memory is context, not permission to run commands or configure tools. Model-writ
|
|
|
163
163
|
|
|
164
164
|
With the SessionStart hook installed (`memoir hooks install`), `memoir auto-refresh` prints a short session brief on stdout, which Claude Code adds to the model's context. Delivery no longer depends on the model choosing to call `memoir_session`. The brief is capped (~300 tokens), silent when there is nothing to say, skipped on compaction, and disabled by `MEMOIR_NO_SESSION_BRIEF`. Preview it with `memoir auto-refresh --show-brief`. It scopes by the hand-written `[project]` text prefix first and the stored project field second; untagged items are shown under their own heading, not presented as facts about the current repository.
|
|
165
165
|
|
|
166
|
-
`push --redact` heuristically redacts staged text, not originals or historical backups. Without it, ordinary memory backups warn and preserve content.
|
|
166
|
+
`push --redact` heuristically redacts staged text, not originals or historical backups. Without it, ordinary memory backups warn and preserve content. Anonymous usage telemetry is **off by default** for new and existing installs. Enable it with `memoir telemetry on`; check with `memoir telemetry status`; turn it off with `memoir telemetry off`. `DO_NOT_TRACK=1` or `MEMOIR_TELEMETRY=0` always disable remote telemetry, even after opting in. No telemetry network calls are made while it is off, including on first run. Local events distinguish execution failure from success; they do not measure answer usefulness.
|
|
167
167
|
|
|
168
168
|
Forgetting propagates on later sync between updated clients. Old clients, snapshots, and Git history can still contain deleted text. Purging every historical copy is separate. Native configuration restore is not atomic across multiple application directories.
|
|
169
169
|
|
package/bin/memoir.js
CHANGED
|
@@ -161,7 +161,7 @@ program
|
|
|
161
161
|
.command('share')
|
|
162
162
|
.description('Share your AI memory via a secure link')
|
|
163
163
|
.option('--only <tools>', 'Only share specific tools (comma-separated)')
|
|
164
|
-
.option('--expires <hours>', 'Link expiry in hours (default: 24)')
|
|
164
|
+
.option('--expires <hours>', 'Link expiry in hours (default: 24, max: 720 / 30 days)')
|
|
165
165
|
.option('--uses <number>', 'Max number of uses (default: 5)')
|
|
166
166
|
.action(async (options) => {
|
|
167
167
|
try {
|
|
@@ -576,7 +576,7 @@ const account = program.command('account').description('Manage your memoir accou
|
|
|
576
576
|
|
|
577
577
|
account
|
|
578
578
|
.command('delete')
|
|
579
|
-
.description('Permanently delete your account
|
|
579
|
+
.description('Permanently delete your account, cloud data, and login')
|
|
580
580
|
.option('--confirm', 'Skip interactive prompt (Node 25 workaround)')
|
|
581
581
|
.action(async (options) => {
|
|
582
582
|
try {
|
|
@@ -766,7 +766,7 @@ program
|
|
|
766
766
|
|
|
767
767
|
program
|
|
768
768
|
.command('telemetry [action]')
|
|
769
|
-
.description('Anonymous usage telemetry: `on`, `off`, or `status`
|
|
769
|
+
.description('Anonymous usage telemetry (off by default): `on`, `off`, or `status`')
|
|
770
770
|
.action(async (action) => {
|
|
771
771
|
try {
|
|
772
772
|
await telemetryCommand(action || 'status');
|
|
@@ -778,8 +778,8 @@ program
|
|
|
778
778
|
|
|
779
779
|
program.hook('postAction', async (thisCommand, actionCommand) => {
|
|
780
780
|
await checkForUpdate();
|
|
781
|
-
// Anonymous, opt-
|
|
782
|
-
// (checkForUpdate), so this adds no perceived latency; no-op
|
|
781
|
+
// Anonymous, opt-in usage event. postAction already awaits a network call
|
|
782
|
+
// (checkForUpdate), so this adds no perceived latency; no-op unless opted in.
|
|
783
783
|
try { await track('cli_command', { command: actionCommand?.name?.() || 'unknown' }); } catch {}
|
|
784
784
|
// Local twin (events.jsonl): command name only, so `memoir recall`/`why`
|
|
785
785
|
// reads are visible next to the writes and syncs already logged there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.6",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cloud/auth.js
CHANGED
|
@@ -170,9 +170,10 @@ async function verifyNoneRemaining(endpoint, headers, label) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// Deletes the data the current user is authorized to delete
|
|
174
|
-
//
|
|
175
|
-
//
|
|
173
|
+
// Deletes the data the current user is authorized to delete, then asks the
|
|
174
|
+
// hosted `account-delete` function to remove the Auth login. That function
|
|
175
|
+
// uses the service role on the server to delete only the JWT subject. This
|
|
176
|
+
// CLI never sends a service-role key or a caller-chosen user id.
|
|
176
177
|
export async function deleteAccount(session) {
|
|
177
178
|
const headers = {
|
|
178
179
|
'Authorization': `Bearer ${session.access_token}`,
|
|
@@ -279,8 +280,36 @@ export async function deleteAccount(session) {
|
|
|
279
280
|
);
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
const identityRes = await fetch(`${SUPABASE_URL}/functions/v1/account-delete`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: {
|
|
286
|
+
...headers,
|
|
287
|
+
'Content-Type': 'application/json',
|
|
288
|
+
},
|
|
289
|
+
body: '{}',
|
|
290
|
+
});
|
|
291
|
+
const identityText = await identityRes.text().catch(() => '');
|
|
292
|
+
let identityBody = {};
|
|
293
|
+
try { identityBody = identityText ? JSON.parse(identityText) : {}; } catch { identityBody = {}; }
|
|
294
|
+
if (!identityRes.ok) {
|
|
295
|
+
if (identityRes.status === 409 && identityBody.error === 'subscription_active') {
|
|
296
|
+
throw new Error(identityBody.message || `Your Pro subscription is still active. Email ${SUPPORT_EMAIL} to cancel it, then run this again. Nothing else was reported as deleted; retry account deletion.`);
|
|
297
|
+
}
|
|
298
|
+
const detail = identityBody.message || identityText.trim();
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Auth identity deletion failed (${identityRes.status})${detail ? ': ' + detail : ''}. ` +
|
|
301
|
+
'Cloud data for this account was already removed; retry account deletion to finish removing the login.'
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (identityBody?.deleted !== true) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
'Auth identity deletion failed: unexpected response. ' +
|
|
307
|
+
'Cloud data for this account was already removed; retry account deletion to finish removing the login.'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
282
311
|
await logout();
|
|
283
|
-
return { localDataDeleted: true, authUserDeleted:
|
|
312
|
+
return { localDataDeleted: true, authUserDeleted: true };
|
|
284
313
|
}
|
|
285
314
|
|
|
286
315
|
export { AUTH_FILE, supaFetch };
|
package/src/commands/login.js
CHANGED
|
@@ -161,7 +161,7 @@ export async function deleteAccountCommand(options = {}) {
|
|
|
161
161
|
const answer = await inquirer.prompt([{
|
|
162
162
|
type: 'input',
|
|
163
163
|
name: 'confirmation',
|
|
164
|
-
message: 'Type DELETE to remove your Memoir cloud data and
|
|
164
|
+
message: 'Type DELETE to remove your Memoir account (cloud data and login):',
|
|
165
165
|
}]);
|
|
166
166
|
|
|
167
167
|
if (answer.confirmation !== 'DELETE') {
|
|
@@ -171,12 +171,14 @@ export async function deleteAccountCommand(options = {}) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
|
-
await deleteAccount(session);
|
|
174
|
+
const result = await deleteAccount(session);
|
|
175
|
+
if (!result?.authUserDeleted) {
|
|
176
|
+
throw new Error('Account deletion did not confirm that the login was removed.');
|
|
177
|
+
}
|
|
175
178
|
console.log('\n' + boxen(
|
|
176
179
|
gradient.pastel(' memoir cloud ') + '\n\n' +
|
|
177
|
-
chalk.green('✔
|
|
178
|
-
chalk.gray('Backups, shared links,
|
|
179
|
-
chalk.yellow('Your authentication identity may still exist in Supabase Auth (removing the login record requires server-side admin access).'),
|
|
180
|
+
chalk.green('✔ Account deleted. Cloud data and this login were removed, and this device was signed out.') + '\n' +
|
|
181
|
+
chalk.gray('Backups, shared links, subscription data, and the authentication identity for this account were removed.'),
|
|
180
182
|
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
181
183
|
) + '\n');
|
|
182
184
|
} catch (error) {
|
package/src/commands/share.js
CHANGED
|
@@ -13,6 +13,16 @@ import { encryptDirectory, createVerifyToken } from '../security/encryption.js';
|
|
|
13
13
|
import { bundleDir } from '../cloud/storage.js';
|
|
14
14
|
import { SUPABASE_URL, SUPABASE_ANON_KEY, STORAGE_BUCKET } from '../cloud/constants.js';
|
|
15
15
|
|
|
16
|
+
// Privacy §8: shares are purged within 30 days of expiry or exhaustion.
|
|
17
|
+
// Cap lifetime so a single --expires value cannot outrun that window by years.
|
|
18
|
+
export const MAX_SHARE_EXPIRES_HOURS = 24 * 30;
|
|
19
|
+
|
|
20
|
+
export function normalizeShareExpiresHours(raw) {
|
|
21
|
+
const n = parseInt(raw, 10);
|
|
22
|
+
if (!Number.isFinite(n) || n <= 0) return 24;
|
|
23
|
+
return Math.min(n, MAX_SHARE_EXPIRES_HOURS);
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export async function shareCommand(options = {}) {
|
|
17
27
|
// Must be logged in to share
|
|
18
28
|
const session = await getSession();
|
|
@@ -107,7 +117,7 @@ export async function shareCommand(options = {}) {
|
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
// Parse options
|
|
110
|
-
const expiresHours =
|
|
120
|
+
const expiresHours = normalizeShareExpiresHours(options.expires);
|
|
111
121
|
const maxUses = parseInt(options.uses) || 5;
|
|
112
122
|
const expiresAt = new Date(Date.now() + expiresHours * 60 * 60 * 1000).toISOString();
|
|
113
123
|
|
package/src/commands/upgrade.js
CHANGED
|
@@ -4,7 +4,7 @@ import boxen from 'boxen';
|
|
|
4
4
|
import gradient from 'gradient-string';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { getSession, getSubscription, supaFetch } from '../cloud/auth.js';
|
|
7
|
-
import { SUPABASE_URL } from '../cloud/constants.js';
|
|
7
|
+
import { SUPABASE_URL, MAX_BACKUPS_FREE, MAX_BACKUPS_PRO } from '../cloud/constants.js';
|
|
8
8
|
|
|
9
9
|
async function createCheckoutSession(session) {
|
|
10
10
|
const res = await fetch(`${SUPABASE_URL}/functions/v1/stripe-checkout`, {
|
|
@@ -57,7 +57,7 @@ export async function upgradeCommand() {
|
|
|
57
57
|
|
|
58
58
|
const freeLabel = currentPlan === 'free' ? 'Free (current)' : 'Free';
|
|
59
59
|
const proLabel = currentPlan === 'pro' ? 'Pro (current)' : 'Pro $15/mo';
|
|
60
|
-
const teamsLabel = 'Teams
|
|
60
|
+
const teamsLabel = 'Teams (coming soon)';
|
|
61
61
|
|
|
62
62
|
const header =
|
|
63
63
|
pad(chalk.bold.white(freeLabel), col1) +
|
|
@@ -67,12 +67,12 @@ export async function upgradeCommand() {
|
|
|
67
67
|
const sep = chalk.gray('─'.repeat(col1 + col2 + 18));
|
|
68
68
|
|
|
69
69
|
const rows = [
|
|
70
|
-
[chalk.gray(
|
|
71
|
-
[chalk.gray('
|
|
72
|
-
[chalk.
|
|
73
|
-
[chalk.gray('
|
|
74
|
-
[chalk.gray('—'),
|
|
75
|
-
[chalk.gray('—'),
|
|
70
|
+
[chalk.gray(`${MAX_BACKUPS_FREE} retained backups`), chalk.white(`${MAX_BACKUPS_PRO} retained backups`), chalk.white(`${MAX_BACKUPS_PRO} retained backups`)],
|
|
71
|
+
[chalk.gray('Community support'), chalk.white('Sync across machines'), chalk.white('Shared team context')],
|
|
72
|
+
[chalk.white('E2E encryption'), chalk.white('E2E encryption'), chalk.white('E2E encryption')],
|
|
73
|
+
[chalk.gray('—'), chalk.gray('—'), chalk.white('Team dashboard')],
|
|
74
|
+
[chalk.gray('—'), chalk.gray('—'), chalk.white('Audit log')],
|
|
75
|
+
[chalk.gray('—'), chalk.gray('—'), chalk.white('SSO & RBAC')],
|
|
76
76
|
];
|
|
77
77
|
|
|
78
78
|
const tableRows = rows.map(([a, b, c]) =>
|
package/src/mcp.js
CHANGED
|
@@ -81,7 +81,8 @@ const server = new McpServer({
|
|
|
81
81
|
// Wrap server.tool ONCE so every registered handler emits an anonymous, no-PII
|
|
82
82
|
// "mcp_tool_used" event on call — the only place that proves memory was actually
|
|
83
83
|
// used (the North Star's activation event). Fire-and-forget; can't block or
|
|
84
|
-
// break a tool response. No-op unless
|
|
84
|
+
// break a tool response. No-op unless the user has opted in and a telemetry
|
|
85
|
+
// key is configured.
|
|
85
86
|
track('mcp_server_start');
|
|
86
87
|
const _registerTool = server.tool.bind(server);
|
|
87
88
|
server.tool = (name, ...rest) => {
|
package/src/telemetry.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// Anonymous, opt-
|
|
1
|
+
// Anonymous, opt-in usage telemetry.
|
|
2
2
|
//
|
|
3
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// problem for a short-lived CLI, and a HARD NO-OP unless a project key is set
|
|
5
|
+
// AND the user has explicitly opted in. Honors DO_NOT_TRACK, CI,
|
|
6
|
+
// MEMOIR_TELEMETRY=0/off/false, and `memoir telemetry off`. We NEVER send PII
|
|
7
|
+
// or any memory contents — only an anonymous install UUID, the event name, the
|
|
8
|
+
// OS, and the CLI version. All output goes to stderr so it can never corrupt
|
|
9
|
+
// the MCP stdio protocol (which speaks JSON-RPC over stdout).
|
|
9
10
|
import fs from 'fs-extra';
|
|
10
11
|
import path from 'path';
|
|
11
12
|
import os from 'os';
|
|
@@ -17,35 +18,68 @@ const VERSION = (() => {
|
|
|
17
18
|
try { return require('../package.json').version; } catch { return 'unknown'; }
|
|
18
19
|
})();
|
|
19
20
|
|
|
20
|
-
// PostHog PROJECT API key (phc_…).
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
21
|
+
// PostHog PROJECT API key (phc_…). PUBLIC client key — same model as posthog-js.
|
|
22
|
+
// Owner: rotating the project is a one-line change of POSTHOG_PROJECT_KEY (and
|
|
23
|
+
// retiring the old PostHog project). MEMOIR_POSTHOG_KEY overrides at runtime.
|
|
24
|
+
// Empty → telemetry is a silent no-op.
|
|
25
|
+
export const POSTHOG_PROJECT_KEY = 'phc_vS7ZKfmZAcGnaCE7Zt4hvwFioJBs6jr8gutyapDpqFXW';
|
|
26
|
+
const POSTHOG_KEY = process.env.MEMOIR_POSTHOG_KEY || POSTHOG_PROJECT_KEY;
|
|
24
27
|
const POSTHOG_HOST = process.env.MEMOIR_POSTHOG_HOST || 'https://us.i.posthog.com';
|
|
25
28
|
|
|
26
29
|
const CONFIG_DIR = process.platform === 'win32'
|
|
27
30
|
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir')
|
|
28
31
|
: path.join(os.homedir(), '.config', 'memoir');
|
|
29
32
|
const ID_FILE = path.join(CONFIG_DIR, 'telemetry-id');
|
|
33
|
+
const OPTIN_FILE = path.join(CONFIG_DIR, 'telemetry-on');
|
|
30
34
|
const OPTOUT_FILE = path.join(CONFIG_DIR, 'telemetry-off');
|
|
31
35
|
const DISCLOSED_FILE = path.join(CONFIG_DIR, 'telemetry-disclosed');
|
|
32
36
|
|
|
37
|
+
function envFlag() {
|
|
38
|
+
return String(process.env.MEMOIR_TELEMETRY || '').toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function envForcesOff() {
|
|
42
|
+
if (['1', 'true'].includes(process.env.DO_NOT_TRACK)) return 'DO_NOT_TRACK is set';
|
|
43
|
+
if (process.env.CI) return 'running in CI';
|
|
44
|
+
if (['0', 'off', 'false'].includes(envFlag())) return 'MEMOIR_TELEMETRY is off';
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fileOptedOut() {
|
|
49
|
+
try { return fs.existsSync(OPTOUT_FILE); } catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fileOptedIn() {
|
|
53
|
+
try { return fs.existsSync(OPTIN_FILE); } catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function envForcesOn() {
|
|
57
|
+
return ['1', 'on', 'true'].includes(envFlag());
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
export function isEnabled() {
|
|
34
61
|
if (!POSTHOG_KEY) return false; // no key → no-op
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
if (envForcesOff()) return false;
|
|
63
|
+
if (fileOptedOut()) return false;
|
|
64
|
+
return envForcesOn() || fileOptedIn(); // default off
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function discardStaleIdentity() {
|
|
68
|
+
try { fs.removeSync(ID_FILE); } catch {}
|
|
69
|
+
try { fs.removeSync(DISCLOSED_FILE); } catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mintInstallId() {
|
|
73
|
+
const id = randomUUID();
|
|
74
|
+
try { fs.ensureDirSync(CONFIG_DIR); fs.writeFileSync(ID_FILE, id); } catch {}
|
|
75
|
+
return id;
|
|
40
76
|
}
|
|
41
77
|
|
|
42
78
|
function getInstallId() {
|
|
43
79
|
try {
|
|
44
80
|
if (fs.existsSync(ID_FILE)) return fs.readFileSync(ID_FILE, 'utf8').trim();
|
|
45
81
|
} catch {}
|
|
46
|
-
|
|
47
|
-
try { fs.ensureDirSync(CONFIG_DIR); fs.writeFileSync(ID_FILE, id); } catch {}
|
|
48
|
-
return id;
|
|
82
|
+
return mintInstallId();
|
|
49
83
|
}
|
|
50
84
|
|
|
51
85
|
function discloseOnce() {
|
|
@@ -54,8 +88,8 @@ function discloseOnce() {
|
|
|
54
88
|
fs.ensureDirSync(CONFIG_DIR);
|
|
55
89
|
fs.writeFileSync(DISCLOSED_FILE, new Date().toISOString());
|
|
56
90
|
process.stderr.write(
|
|
57
|
-
'\n memoir
|
|
58
|
-
'
|
|
91
|
+
'\n memoir sends anonymous, no-PII usage stats because telemetry is opted in.\n' +
|
|
92
|
+
' Disable anytime: `memoir telemetry off` (or set DO_NOT_TRACK=1).\n\n'
|
|
59
93
|
);
|
|
60
94
|
} catch {}
|
|
61
95
|
}
|
|
@@ -64,7 +98,10 @@ function discloseOnce() {
|
|
|
64
98
|
// touches stdout. Callers may await (CLI) or not (MCP) — both are safe.
|
|
65
99
|
export async function capture(event, properties = {}) {
|
|
66
100
|
try {
|
|
67
|
-
if (!isEnabled())
|
|
101
|
+
if (!isEnabled()) {
|
|
102
|
+
discardStaleIdentity();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
68
105
|
discloseOnce();
|
|
69
106
|
const ctrl = new AbortController();
|
|
70
107
|
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
@@ -90,19 +127,44 @@ export async function capture(event, properties = {}) {
|
|
|
90
127
|
export async function telemetryCommand(action = 'status') {
|
|
91
128
|
const a = String(action).toLowerCase();
|
|
92
129
|
if (a === 'off') {
|
|
93
|
-
try {
|
|
130
|
+
try {
|
|
131
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
132
|
+
await fs.writeFile(OPTOUT_FILE, '1');
|
|
133
|
+
await fs.remove(OPTIN_FILE);
|
|
134
|
+
} catch {}
|
|
135
|
+
discardStaleIdentity();
|
|
94
136
|
console.log(' Telemetry disabled. memoir will not send any usage events.');
|
|
95
137
|
return;
|
|
96
138
|
}
|
|
97
139
|
if (a === 'on') {
|
|
98
|
-
try {
|
|
99
|
-
|
|
140
|
+
try {
|
|
141
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
142
|
+
await fs.writeFile(OPTIN_FILE, '1');
|
|
143
|
+
await fs.remove(OPTOUT_FILE);
|
|
144
|
+
} catch {}
|
|
145
|
+
discardStaleIdentity();
|
|
146
|
+
mintInstallId();
|
|
147
|
+
const blocked = envForcesOff();
|
|
148
|
+
if (blocked) {
|
|
149
|
+
console.log(` Telemetry opted in, but currently OFF — ${blocked}.`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(' Telemetry enabled (anonymous, no PII). Disable with `memoir telemetry off`.');
|
|
152
|
+
}
|
|
100
153
|
return;
|
|
101
154
|
}
|
|
102
155
|
// status
|
|
103
156
|
const reason = !POSTHOG_KEY ? 'no project key configured'
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
: envForcesOff()
|
|
158
|
+
|| (fileOptedOut() ? 'opted out (`memoir telemetry off`)' : null);
|
|
159
|
+
if (reason) {
|
|
160
|
+
discardStaleIdentity();
|
|
161
|
+
console.log(` Telemetry: OFF — ${reason}.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (envForcesOn() || fileOptedIn()) {
|
|
165
|
+
console.log(' Telemetry: ON — anonymous usage events (no PII). Disable with `memoir telemetry off`.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
discardStaleIdentity();
|
|
169
|
+
console.log(' Telemetry: OFF — off by default. Enable with `memoir telemetry on`.');
|
|
108
170
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Expired/exhausted share purge lives in supabase/functions/purge-expired-shares.
|
|
2
|
+
-- It removes objects through the Storage API, then deletes matching rows.
|
|
3
|
+
-- Direct SQL deletes against the Storage catalog orphan files (and current projects reject them).
|
|
4
|
+
--
|
|
5
|
+
-- This migration only drops the unsafe SQL purge if an earlier draft applied it.
|
|
6
|
+
-- There is no pg_cron → SQL delete path.
|
|
7
|
+
--
|
|
8
|
+
-- MANUAL STEPS (owner):
|
|
9
|
+
-- 1. Apply this file (no-op drop if the SQL purge was never created).
|
|
10
|
+
-- 2. Set MEMOIR_PURGE_SECRET on the purge-expired-shares function.
|
|
11
|
+
-- 3. Deploy supabase/functions/purge-expired-shares (verify_jwt = false;
|
|
12
|
+
-- the function requires header x-memoir-purge-secret).
|
|
13
|
+
-- 4. Schedule a daily POST to that function with the secret header.
|
|
14
|
+
-- Do not schedule `select memoir_purge_expired_shares()`.
|
|
15
|
+
begin;
|
|
16
|
+
|
|
17
|
+
drop function if exists public.memoir_purge_expired_shares();
|
|
18
|
+
|
|
19
|
+
commit;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- shared_links.expires_at must be set so the purge always has an expiry.
|
|
2
|
+
-- Null-expiry rows under their download limit were never selected.
|
|
3
|
+
--
|
|
4
|
+
-- MANUAL STEP (owner): apply by SQL if `supabase db push` still mis-pairs
|
|
5
|
+
-- 12-digit history. Do not skip this; the edge function assumes the column
|
|
6
|
+
-- is present and not null after this lands.
|
|
7
|
+
begin;
|
|
8
|
+
|
|
9
|
+
update public.shared_links
|
|
10
|
+
set expires_at = coalesce(created_at, now()) + interval '30 days'
|
|
11
|
+
where expires_at is null;
|
|
12
|
+
|
|
13
|
+
alter table public.shared_links
|
|
14
|
+
alter column expires_at set default (now() + interval '24 hours');
|
|
15
|
+
|
|
16
|
+
alter table public.shared_links
|
|
17
|
+
alter column expires_at set not null;
|
|
18
|
+
|
|
19
|
+
commit;
|