feat: always offer the 30-day trial (#3798)

The 7/14/30 trial-length split has been called — 30 won on signups.

pickCmemProTrialDays() returns 30 instead of drawing an arm; the signup
URL, offer copy, and trial-start call already took the length as a
parameter. 7 and 14 stay valid on cmem.ai and in parseCmemProTrialDays,
we just stop selecting them.

The session-start banner, context banner, welcome hint, viewer header,
and cursor-hooks docs said "7-day trial" and linked to /pro with no
trial param — cmem.ai buckets a paramless visitor into 7/14/30 itself,
so those clicks could still land on a 7-day trial. They now read a
single PRO_TRIAL_DAYS constant and pass trial=30 explicitly, with a test
keeping the viewer's mirrored copy in step.
This commit is contained in:
Alex Newman
2026-08-29 16:21:12 -07:00
committed by GitHub
parent 8fe94351af
commit 2e3f42e423
8 changed files with 57 additions and 37 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ Restart Cursor to load the hooks.
- Browse sessions, observations, and summaries
- Search your project history
**Get 2x more use out of your Max plan for free (7-day trial, $30/mo)** → https://cmem.ai/pro?from=docs
**Get 2x more use out of your Max plan for free (30-day trial, $30/mo)** → https://cmem.ai/pro?from=docs&trial=30
### Via MCP Tools (if enabled)
- claude-mem provides search tools via MCP
+1 -1
View File
@@ -55,7 +55,7 @@ Context is automatically injected via Cursor's **Rules** system:
- **Web Viewer**: Access context at `http://localhost:37777`
- **Manual Request**: Ask the agent to search memory
**Get 2x more use out of your Max plan for free (7-day trial, $30/mo)** → https://cmem.ai/pro?from=docs
**Get 2x more use out of your Max plan for free (30-day trial, $30/mo)** → https://cmem.ai/pro?from=docs&trial=30
See [CONTEXT-INJECTION.md](CONTEXT-INJECTION.md) for details.
+2 -2
View File
@@ -12,7 +12,7 @@ Use claude-mem's persistent memory in Cursor without a Claude Code subscription.
- **Context injection** via `.cursor/rules/` - relevant history included in every chat
- **Web viewer** at http://localhost:37777 - browse and search your project history
**Get 2x more use out of your Max plan for free (7-day trial, $30/mo)** → https://cmem.ai/pro?from=docs
**Get 2x more use out of your Max plan for free (30-day trial, $30/mo)** → https://cmem.ai/pro?from=docs&trial=30
**Why This Matters**: Every Cursor session starts fresh. Claude-mem bridges that gap - your AI agent builds cumulative knowledge about your codebase, decisions, and patterns over time.
@@ -149,7 +149,7 @@ The worker runs in the background and handles:
4. **Open the web viewer**: http://localhost:37777
✨ **Get 2x more use out of your Max plan for free (7-day trial, $30/mo)** → https://cmem.ai/pro?from=docs
✨ **Get 2x more use out of your Max plan for free (30-day trial, $30/mo)** → https://cmem.ai/pro?from=docs&trial=30
## How It Works
+8 -5
View File
@@ -70,11 +70,14 @@ export function parseCmemProTrialDays(value: unknown): CmemProTrialDays | null {
return parsed === 7 || parsed === 14 || parsed === 30 ? parsed : null;
}
/** Pick one arm once at installer entry; callers carry it through the run. */
export function pickCmemProTrialDays(random: () => number = Math.random): CmemProTrialDays {
return CMEM_PRO_TRIAL_LENGTHS[
Math.floor(random() * CMEM_PRO_TRIAL_LENGTHS.length)
] ?? CMEM_PRO_TRIAL_LENGTHS[0];
/**
* The length offered at installer entry; callers carry it through the run.
* The 7/14/30 split ran as a test and 30 won, so every run offers 30 — the
* other two stay valid on cmem.ai and in parseCmemProTrialDays, we just stop
* selecting them. Restoring the split means putting the draw back here.
*/
export function pickCmemProTrialDays(): CmemProTrialDays {
return 30;
}
/** Installer-specific offer copy, kept next to the selected duration. */
+2 -3
View File
@@ -1447,12 +1447,11 @@ export function parseStoredTrialState(
};
}
/** Reuse a persisted arm across installer runs; only a fresh flow randomizes. */
/** Reuse a persisted arm across installer runs; a fresh flow takes the default. */
export function resolveInstallerTrialDays(
prior: StoredTrialState | null,
random: () => number = Math.random,
): CmemProTrialDays {
return prior?.trialDays ?? pickCmemProTrialDays(random);
return prior?.trialDays ?? pickCmemProTrialDays();
}
function readStoredTrialState(): StoredTrialState | null {
+10 -3
View File
@@ -1,5 +1,5 @@
/**
* cmem Pro 7-day-trial promo copy the single source of truth for every place
* cmem Pro trial promo copy the single source of truth for every place
* claude-mem tells an existing user the trial exists.
*
* Almost nobody running the free plugin knows the trial is there, so the pitch
@@ -13,6 +13,13 @@
* Change both together.
*/
/**
* Trial length offered, in days. Mirrored in the viewer's copy of this module.
* cmem.ai buckets a visitor into 7/14/30 when no `trial=` is passed, so every
* link below states it explicitly rather than letting the page choose.
*/
export const PRO_TRIAL_DAYS = 30;
/** Landing page for the trial (cmem-pro `src/app/(landing)/pro/page.tsx`). */
export const PRO_TRIAL_URL = 'https://cmem.ai/pro';
@@ -32,11 +39,11 @@ export type ProPromoSource =
/** Trial landing URL tagged with the surface the user clicked from. */
export function proTrialUrl(source: ProPromoSource): string {
return `${PRO_TRIAL_URL}?from=${source}`;
return `${PRO_TRIAL_URL}?from=${source}&trial=${PRO_TRIAL_DAYS}`;
}
/** The offer itself, without a URL — for surfaces that link separately. */
export const PRO_TRIAL_PITCH = 'Get 2x more use out of your Max plan for free (7-day trial, $30/mo)';
export const PRO_TRIAL_PITCH = `Get 2x more use out of your Max plan for free (${PRO_TRIAL_DAYS}-day trial, $30/mo)`;
/**
* One-line pitch + link, for plain-text surfaces (hook banners, welcome hint).
+9 -3
View File
@@ -6,10 +6,16 @@
* together when the offer or the URL moves.
*/
/** Trial landing URL, tagged so cmem.ai can attribute viewer-sourced signups. */
export const PRO_TRIAL_URL = 'https://cmem.ai/pro?from=viewer';
/** Trial length offered, in days. Mirrors PRO_TRIAL_DAYS in the shared module. */
export const PRO_TRIAL_DAYS = 30;
export const PRO_TRIAL_PITCH = 'Get 2x more use out of your Max plan for free (7-day trial, $30/mo)';
/**
* Trial landing URL, tagged so cmem.ai can attribute viewer-sourced signups.
* `trial=` is explicit without it cmem.ai buckets the visitor into 7/14/30.
*/
export const PRO_TRIAL_URL = `https://cmem.ai/pro?from=viewer&trial=${PRO_TRIAL_DAYS}`;
export const PRO_TRIAL_PITCH = `Get 2x more use out of your Max plan for free (${PRO_TRIAL_DAYS}-day trial, $30/mo)`;
/** Header CTA label. The full pitch rides in the title/aria attributes. */
export const PRO_TRIAL_SHORT = 'Get 2x more from your Max plan';
+24 -19
View File
@@ -13,6 +13,12 @@ import {
parseCmemProTrialDays,
pickCmemProTrialDays,
} from '../../src/npx-cli/cmem-pro-costs.js';
import { PRO_TRIAL_DAYS, PRO_TRIAL_PITCH, proTrialUrl } from '../../src/shared/pro-promo.js';
import {
PRO_TRIAL_DAYS as VIEWER_TRIAL_DAYS,
PRO_TRIAL_PITCH as VIEWER_PITCH,
PRO_TRIAL_URL as VIEWER_URL,
} from '../../src/ui/viewer/constants/promo.js';
import {
captureInstallerProOfferViewed,
parseStoredTrialState,
@@ -48,11 +54,21 @@ afterEach(() => {
});
describe('installer trial length', () => {
it('selects each of the three accepted trial arms', () => {
it('offers 30 days on every run while 7 and 14 stay accepted values', () => {
expect(CMEM_PRO_TRIAL_LENGTHS).toEqual([7, 14, 30]);
expect(pickCmemProTrialDays(() => 0)).toBe(7);
expect(pickCmemProTrialDays(() => 0.34)).toBe(14);
expect(pickCmemProTrialDays(() => 0.99)).toBe(30);
expect(pickCmemProTrialDays()).toBe(30);
});
it('states the same length and an explicit trial= on every promo surface', () => {
// Without `trial=`, cmem.ai buckets the visitor into 7/14/30 itself.
expect(PRO_TRIAL_DAYS).toBe(30);
expect(PRO_TRIAL_PITCH).toContain('30-day trial');
for (const source of ['session-start', 'context-banner', 'welcome-hint', 'viewer', 'docs'] as const) {
expect(proTrialUrl(source)).toBe(`https://cmem.ai/pro?from=${source}&trial=30`);
}
// The viewer cannot import the shared module (rootDir); keep them in step.
expect([VIEWER_TRIAL_DAYS, VIEWER_PITCH, VIEWER_URL])
.toEqual([PRO_TRIAL_DAYS, PRO_TRIAL_PITCH, proTrialUrl('viewer')]);
});
it('accepts only exact persisted 7/14/30 values', () => {
@@ -162,35 +178,24 @@ describe('installer trial length', () => {
CLAUDE_MEM_PRO_TRIAL_STATE: 'link_sent',
CLAUDE_MEM_PRO_TRIAL_DAYS: '14',
});
let randomCalls = 0;
expect(prior?.trialDays).toBe(14);
expect(resolveInstallerTrialDays(prior, () => {
randomCalls += 1;
return 0.99;
})).toBe(14);
expect(randomCalls).toBe(0);
expect(resolveInstallerTrialDays(prior)).toBe(14);
});
it('picks exactly once for a fresh or invalid legacy state', () => {
it('falls back to the default arm for a fresh or invalid legacy state', () => {
const invalidPrior = parseStoredTrialState({
CLAUDE_MEM_PRO_TRIAL_EMAIL: 'person@example.com',
CLAUDE_MEM_PRO_TRIAL_STATE: 'link_sent',
CLAUDE_MEM_PRO_TRIAL_DAYS: 'forever',
});
let randomCalls = 0;
expect(invalidPrior?.trialDays).toBeNull();
expect(resolveInstallerTrialDays(invalidPrior, () => {
randomCalls += 1;
return 0.99;
})).toBe(30);
expect(randomCalls).toBe(1);
expect(resolveInstallerTrialDays(invalidPrior)).toBe(30);
});
it('chooses once at funnel entry and carries that value through every installer path', () => {
expect(installSource.match(/resolveInstallerTrialDays\(storedTrialState\)/g)).toHaveLength(1);
expect(installSource.match(/pickCmemProTrialDays\(random\)/g)).toHaveLength(1);
expect(installSource.match(/pickCmemProTrialDays\(\)/g)).toHaveLength(1);
expect(installSource).toContain('promptProTrialOptIn(version, trialDays, storedTrialState)');
expect(installSource).toContain('promptProvider(options, trialDays)');
expect(installSource).toContain('cmemProSignupUrl(trialDays)');