fix(cli): persistent anonymous telemetry ID + pass source=CLI in all API calls (#4789)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -116,6 +116,7 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.expires) payload.expiration_date = opts.expires;
|
||||
if (opts.categories) payload.categories = opts.categories;
|
||||
if (opts.enableGraph) payload.enable_graph = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
return (await this._request("POST", "/v1/memories/", {
|
||||
json: payload,
|
||||
@@ -176,6 +177,7 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.keyword) payload.keyword_search = true;
|
||||
if (opts.fields) payload.fields = opts.fields;
|
||||
if (opts.enableGraph) payload.enable_graph = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v2/memories/search/", {
|
||||
json: payload,
|
||||
@@ -186,10 +188,9 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async listMemories(
|
||||
@@ -227,6 +228,7 @@ export class PlatformBackend implements Backend {
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
if (opts.enableGraph) payload.enable_graph = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v2/memories/", {
|
||||
json: payload,
|
||||
@@ -245,6 +247,7 @@ export class PlatformBackend implements Backend {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (content) payload.text = content;
|
||||
if (metadata) payload.metadata = metadata;
|
||||
payload.source = "CLI";
|
||||
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||
json: payload,
|
||||
})) as Record<string, unknown>;
|
||||
@@ -255,7 +258,7 @@ export class PlatformBackend implements Backend {
|
||||
opts: DeleteOptions = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (opts.all) {
|
||||
const params: Record<string, string> = {};
|
||||
const params: Record<string, string> = { source: "CLI" };
|
||||
if (opts.userId) params.user_id = opts.userId;
|
||||
if (opts.agentId) params.agent_id = opts.agentId;
|
||||
if (opts.appId) params.app_id = opts.appId;
|
||||
@@ -265,10 +268,9 @@ export class PlatformBackend implements Backend {
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
if (memoryId) {
|
||||
return (await this._request(
|
||||
"DELETE",
|
||||
`/v1/memories/${memoryId}/`,
|
||||
)) as Record<string, unknown>;
|
||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
throw new Error("Either memoryId or --all is required");
|
||||
}
|
||||
@@ -291,6 +293,7 @@ export class PlatformBackend implements Backend {
|
||||
result = (await this._request(
|
||||
"DELETE",
|
||||
`/v2/entities/${entityType}/${entityId}/`,
|
||||
{ params: { source: "CLI" } },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -31,10 +31,15 @@ export interface DefaultsConfig {
|
||||
enableGraph: boolean;
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
anonymousId: string;
|
||||
}
|
||||
|
||||
export interface Mem0Config {
|
||||
version: number;
|
||||
defaults: DefaultsConfig;
|
||||
platform: PlatformConfig;
|
||||
telemetry: TelemetryConfig;
|
||||
}
|
||||
|
||||
export function createDefaultConfig(): Mem0Config {
|
||||
@@ -52,6 +57,9 @@ export function createDefaultConfig(): Mem0Config {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
userEmail: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,6 +88,9 @@ export function loadConfig(): Mem0Config {
|
||||
config.defaults.appId = defaults.app_id ?? "";
|
||||
config.defaults.runId = defaults.run_id ?? "";
|
||||
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
||||
|
||||
const telemetry = data.telemetry ?? {};
|
||||
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||
}
|
||||
|
||||
// Environment variable overrides
|
||||
@@ -119,6 +130,9 @@ export function saveConfig(config: Mem0Config): void {
|
||||
base_url: config.platform.baseUrl,
|
||||
user_email: config.platform.userEmail,
|
||||
},
|
||||
telemetry: {
|
||||
anonymous_id: config.telemetry.anonymousId,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { CONFIG_FILE, loadConfig } from "./config.js";
|
||||
import { CONFIG_FILE, loadConfig, saveConfig } from "./config.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
@@ -29,11 +29,34 @@ function isTelemetryEnabled(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a persistent per-machine anonymous ID, generating one if needed.
|
||||
*
|
||||
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
||||
* repeat runs on the same machine share one PostHog identity instead of
|
||||
* collapsing into a single shared fallback string.
|
||||
*/
|
||||
function getOrCreateAnonymousId(): string {
|
||||
const config = loadConfig();
|
||||
if (config.telemetry.anonymousId) {
|
||||
return config.telemetry.anonymousId;
|
||||
}
|
||||
|
||||
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
config.telemetry.anonymousId = newId;
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore persistence failure — still return the generated ID */
|
||||
}
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stable anonymous identifier for the current user.
|
||||
*
|
||||
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
|
||||
* Matches the SDK pattern in mem0-ts/src/client/mem0.ts.
|
||||
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
||||
* persistent per-machine anonymous ID.
|
||||
*/
|
||||
function getDistinctId(): string {
|
||||
try {
|
||||
@@ -47,7 +70,11 @@ function getDistinctId(): string {
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return "anonymous-cli";
|
||||
try {
|
||||
return getOrCreateAnonymousId();
|
||||
} catch {
|
||||
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +96,25 @@ export function captureEvent(
|
||||
const config = loadConfig();
|
||||
const distinctId = preResolvedEmail || getDistinctId();
|
||||
|
||||
// Detect anonymous → identified transition. If a stored anonymous_id
|
||||
// exists and we just resolved to a real identity, fire a one-shot
|
||||
// $identify event so PostHog stitches the pre-signup history onto
|
||||
// the authenticated profile. Clear the stored id so we don't re-alias.
|
||||
let anonIdToAlias: string | null = null;
|
||||
if (
|
||||
distinctId &&
|
||||
!distinctId.startsWith("cli-anon-") &&
|
||||
config.telemetry.anonymousId
|
||||
) {
|
||||
anonIdToAlias = config.telemetry.anonymousId;
|
||||
config.telemetry.anonymousId = "";
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore — alias may double-fire next run, harmless */
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
@@ -92,6 +138,7 @@ export function captureEvent(
|
||||
mem0ApiKey: config.platform.apiKey || "",
|
||||
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
|
||||
configPath: CONFIG_FILE,
|
||||
anonDistinctIdToAlias: anonIdToAlias,
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
|
||||
@@ -94,6 +94,19 @@ async function sendPosthogEvent(posthogHost, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendIdentifyEvent(ctx, payload, anonId) {
|
||||
const identifyPayload = {
|
||||
api_key: payload.api_key,
|
||||
event: "$identify",
|
||||
distinct_id: payload.distinct_id,
|
||||
properties: {
|
||||
$anon_distinct_id: anonId,
|
||||
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
|
||||
},
|
||||
};
|
||||
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = JSON.parse(process.argv[2]);
|
||||
const payload = ctx.payload;
|
||||
@@ -102,6 +115,14 @@ async function main() {
|
||||
await resolveAndCacheEmail(ctx, payload);
|
||||
}
|
||||
|
||||
// Fire $identify *after* email resolution so PostHog links the stored
|
||||
// anonymous id directly to the final identity (email, not the api-key
|
||||
// hash). The regular event is sent next so it lands under the merged
|
||||
// profile.
|
||||
if (ctx.anonDistinctIdToAlias) {
|
||||
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
|
||||
}
|
||||
|
||||
await sendPosthogEvent(ctx.posthogHost, payload);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user