mirror of
https://github.com/thedotmack/claude-mem.git
synced 2026-09-20 04:23:02 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)');
|
||||
|
||||
Reference in New Issue
Block a user