Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions apps/minting-service/src/lib/email.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { renderLicenseEmail } from './email.js';
import { renderLicenseEmail, renderRevocationEmail } from './email.js';

describe('renderLicenseEmail', () => {
it('includes the token wrapped in BEGIN/END delimiters in the text body', () => {
Expand All @@ -16,15 +16,19 @@ describe('renderLicenseEmail', () => {
expect(out.text).toContain('-----END THREADPLANE LICENSE-----');
});

it('subject includes tier and seat count with plural s for seats > 1', () => {
it('uses the public Pro plan name and plural seats in customer-facing copy', () => {
const out = renderLicenseEmail({
tier: 'developer_seat',
seats: 3,
token: 't.s',
expiresAt: new Date('2027-04-20T00:00:00Z'),
stripeCustomerId: 'cus_test',
});
expect(out.subject).toBe('Your Threadplane license — developer_seat (3 seats)');
expect(out.subject).toBe('Your Threadplane Pro license — 3 seats');
expect(out.text).toContain('Plan: Pro');
expect(out.html).toContain('<strong>Plan:</strong> Pro');
expect(out.subject).not.toContain('developer_seat');
expect(out.text).not.toContain('developer_seat');
});

it('subject uses singular seat for seats === 1', () => {
Expand All @@ -35,7 +39,7 @@ describe('renderLicenseEmail', () => {
expiresAt: new Date('2027-04-20T00:00:00Z'),
stripeCustomerId: 'cus_test',
});
expect(out.subject).toBe('Your Threadplane license — team (1 seat)');
expect(out.subject).toBe('Your Threadplane Team license — 1 seat');
});

it('includes ISO 8601 UTC expiry in text body', () => {
Expand All @@ -62,3 +66,15 @@ describe('renderLicenseEmail', () => {
expect(out.html).toContain('BEGIN THREADPLANE LICENSE');
});
});

describe('renderRevocationEmail', () => {
it('describes the record change without claiming offline signature checks perform revocation lookup', () => {
const out = renderRevocationEmail({ tier: 'developer_seat' });

expect(out.text).toContain('Threadplane Pro license');
expect(out.text).toContain('marked revoked in Threadplane records');
expect(out.text).toContain('Runtime verification remains offline');
expect(out.text).not.toContain('fail signature checks');
expect(out.html).not.toContain('developer_seat');
});
});
23 changes: 14 additions & 9 deletions apps/minting-service/src/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ export interface RenderedEmail {
html: string;
}

function publicTierName(tier: MintableTier): string {
return tier === 'developer_seat' ? 'Pro' : 'Team';
}

/**
* Pure: render the subject / text / html for a license delivery email.
*/
export function renderLicenseEmail(vars: LicenseEmailVars): RenderedEmail {
const seatWord = vars.seats === 1 ? 'seat' : 'seats';
const subject = `Your Threadplane license — ${vars.tier} (${vars.seats} ${seatWord})`;
const planName = publicTierName(vars.tier);
const subject = `Your Threadplane ${planName} license — ${vars.seats} ${seatWord}`;
const expiresIso = vars.expiresAt.toISOString();

const portal = portalUrl(vars.stripeCustomerId);
Expand All @@ -47,7 +52,7 @@ the link at the bottom of this email.
${vars.token}
-----END THREADPLANE LICENSE-----

Tier: ${vars.tier}
Plan: ${planName}
Seats: ${vars.seats}
Expires: ${expiresIso}

Expand All @@ -72,7 +77,7 @@ Questions: reply to this email.
<pre style="white-space:pre-wrap;word-break:break-all;font-family:monospace;font-size:12px;background:#f4f4f4;padding:12px;border-radius:4px">-----BEGIN THREADPLANE LICENSE-----
${escapeHtml(vars.token)}
-----END THREADPLANE LICENSE-----</pre>
<p><strong>Tier:</strong> ${escapeHtml(vars.tier)}<br>
<p><strong>Plan:</strong> ${escapeHtml(planName)}<br>
<strong>Seats:</strong> ${vars.seats}<br>
<strong>Expires:</strong> ${escapeHtml(expiresIso)}</p>
<p><strong>Installation:</strong></p>
Expand Down Expand Up @@ -131,20 +136,20 @@ export interface RevocationEmailVars {

export function renderRevocationEmail(vars: RevocationEmailVars): RenderedEmail {
const subject = `Your Threadplane license has been revoked`;
const planName = publicTierName(vars.tier);

const text = `Your Threadplane ${vars.tier} license has been revoked because the
underlying payment was refunded.
const text = `Your Threadplane ${planName} license has been marked revoked in Threadplane records because the underlying payment was refunded.

The token previously delivered will fail signature checks at boot and
@threadplane/chat will fall back to a noncommercial-use warning.
Runtime verification remains offline and does not make a revocation lookup.
Continued use must remain within the applicable license terms.

If you believe this is in error, reply to this email.

-- The Threadplane team
`;

const html = `<p>Your Threadplane <strong>${escapeHtml(vars.tier)}</strong> license has been revoked because the underlying payment was refunded.</p>
<p>The token previously delivered will fail signature checks at boot and <code>@threadplane/chat</code> will fall back to a noncommercial-use warning.</p>
const html = `<p>Your Threadplane <strong>${escapeHtml(planName)}</strong> license has been marked revoked in Threadplane records because the underlying payment was refunded.</p>
<p>Runtime verification remains offline and does not make a revocation lookup. Continued use must remain within the applicable license terms.</p>
<p>If you believe this is in error, reply to this email.</p>
<p>-- The Threadplane team</p>
`;
Expand Down
10 changes: 8 additions & 2 deletions apps/website/content/AGENTS.md.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Threadplane v@VERSION@

Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
Production-ready chat, thread/history/branch UI, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.

## Licensing and deployment boundary
- Most Threadplane packages are MIT-licensed and free for any use.
- `@threadplane/chat` is source-available under PolyForm Noncommercial 1.0.0, includes a good-faith 30-day commercial evaluation, and requires a Threadplane Commercial license for commercial production.
- Threadplane runs inside the customer's Angular application. Agent runtime, models, storage, checkpointing, retention, authorization, and hosting remain customer-operated.
- License-token verification is offline and advisory; it makes no runtime call to Threadplane and never blocks rendering.

## Install
npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked
Expand Down Expand Up @@ -34,7 +40,7 @@ export class ChatComponent {
```

## Key patterns
- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`
- Thread selection: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`; actual durability and cross-device persistence depend on the connected runtime and persistence layer
- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself
Expand Down
10 changes: 8 additions & 2 deletions apps/website/content/CLAUDE.md.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Threadplane v@VERSION@

Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
Production-ready chat, thread/history/branch UI, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.

## Licensing and deployment boundary
- Most Threadplane packages are MIT-licensed and free for any use.
- `@threadplane/chat` is source-available under PolyForm Noncommercial 1.0.0, includes a good-faith 30-day commercial evaluation, and requires a Threadplane Commercial license for commercial production.
- Threadplane runs inside the customer's Angular application. Agent runtime, models, storage, checkpointing, retention, authorization, and hosting remain customer-operated.
- License-token verification is offline and advisory; it makes no runtime call to Threadplane and never blocks rendering.

## Install
npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked
Expand Down Expand Up @@ -34,7 +40,7 @@ export class ChatComponent {
```

## Key patterns
- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`
- Thread selection: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`; actual durability and cross-device persistence depend on the connected runtime and persistence layer
- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A complete walkthrough for installing `@threadplane/chat` in an Angular 20+ appl
This guide goes deeper than the Quick Start: license activation, peer dependencies, and the warnings you'll see in the console.

<Callout type="info" title="Just bought a license?">
This guide is written for customers who purchased a Developer Seat, Team, or Enterprise plan and received a `THREADPLANE_LICENSE` token by email. If you're evaluating `@threadplane/chat` for noncommercial use, you can skip the license steps — the library runs without a token (with a one-time advisory warning).
This guide is written for customers who purchased a Pro, Team, or Enterprise plan and received a `THREADPLANE_LICENSE` token by email. If you're using `@threadplane/chat` within permitted noncommercial scope, you can skip the license steps. Commercial evaluation is permitted for 30 calendar days from first commercial use without registration; the library's token check remains advisory.
</Callout>

## Prerequisites
Expand All @@ -21,7 +21,7 @@ Required for the Angular build toolchain. `node --version` should report `v18` o
The chat UI needs something to talk to. Two officially supported adapters cover virtually every backend:

- **`@threadplane/langgraph`** — pick this if your backend is LangGraph or LangGraph Platform.
- **`@threadplane/ag-ui`** — pick this for any AG-UI compatible backend (CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit runtime).
- **`@threadplane/ag-ui`** — pick this for an AG-UI-compatible backend such as CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, or AWS Strands.

Both adapters expose the same `Agent` contract to `@threadplane/chat`, so swapping later is a one-line change. If you don't have a backend yet, use `mockAgent()` from `@threadplane/chat` to wire up the UI first.
</Step>
Expand Down
33 changes: 28 additions & 5 deletions apps/website/e2e/website.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,35 @@ test('landing page license copy distinguishes MIT packages from commercially lic
await expect(main).not.toContainText('MIT · No signup required · App telemetry off by default');
});

test('pricing page shows plan cards', async ({ page }) => {
test('pricing page presents the four-stage journey and licensing boundary', async ({ page }) => {
await page.goto('/pricing');
await expect(page.getByText('Community').first()).toBeVisible();
await expect(page.getByText('Developer Seat').first()).toBeVisible();
await expect(page.getByText('Team').first()).toBeVisible();
await expect(page.getByText('Enterprise').first()).toBeVisible();
const plans = page.locator('.pricing-plan-card');
await expect(plans).toHaveCount(4);
await expect(plans.nth(0).getByRole('heading', { level: 3 })).toHaveText('Developer');
await expect(plans.nth(1).getByRole('heading', { level: 3 })).toHaveText('Pro');
await expect(plans.nth(2).getByRole('heading', { level: 3 })).toHaveText('Team');
await expect(plans.nth(3).getByRole('heading', { level: 3 })).toHaveText('Enterprise');
await expect(plans.nth(0)).toContainText('For permitted noncommercial use');
await expect(plans.nth(0)).toContainText('30-day commercial evaluation');
await expect(page.getByText('No Threadplane cloud').first()).toBeVisible();
});

test('pricing page preserves public-to-internal checkout mappings', async ({ page }) => {
await page.goto('/pricing');

await expect(page.getByRole('button', { name: 'Get Pro' }).locator('xpath=ancestor::form/input[@name="tier"]')).toHaveValue('developer_seat');
await expect(page.getByRole('button', { name: 'Get Team' }).locator('xpath=ancestor::form/input[@name="tier"]')).toHaveValue('team');
await expect(page.getByRole('link', { name: 'Start free' })).toHaveAttribute('href', /npmjs\.com\/package\/@threadplane\/chat/);
await expect(page.getByRole('link', { name: 'Talk to Sales' })).toHaveAttribute('href', '/contact?source=pricing_tier_enterprise');
});

test('pricing page is responsive without page-level horizontal overflow', async ({ page }) => {
for (const width of [375, 768, 1280]) {
await page.setViewportSize({ width, height: 900 });
await page.goto('/pricing');
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
expect(overflow, `pricing at ${width}px`).toBeLessThanOrEqual(1);
}
});

test('pricing page lead form validates required fields', async ({ page }) => {
Expand Down
8 changes: 4 additions & 4 deletions apps/website/emails/drip-angular-followup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export function dripAngularFollowupHtml(day: number): { subject: string; html: s

if (day === 10) {
return {
subject: 'The pilot program includes hands-on integration',
subject: 'An optional eight-week path from pilot to production',
html: wrapEmail({
body: `
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Pilot Program</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(0,64,144,0.08)">
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Week 1</strong> · Integration &amp; first stream</p>
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Month 1</strong> · First agent in staging</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#004090">Month 3</strong> · Production deployment</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#004090">Week 8</strong> · Production readiness</p>
</div>
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
`,
Expand Down
8 changes: 4 additions & 4 deletions apps/website/emails/drip-chat-followup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export function dripChatFollowupHtml(day: number): { subject: string; html: stri

if (day === 10) {
return {
subject: 'The pilot program includes hands-on integration',
subject: 'An optional eight-week path from pilot to production',
html: wrapEmail({
body: `
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#5a00c8;font-weight:700;margin:0 0 8px">Pilot Program</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(90,0,200,0.08)">
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#5a00c8">Week 1</strong> · Integration &amp; first stream</p>
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#5a00c8">Month 1</strong> · First agent in staging</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#5a00c8">Month 3</strong> · Production deployment</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#5a00c8">Week 8</strong> · Production readiness</p>
</div>
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#5a00c8;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
`,
Expand Down
8 changes: 4 additions & 4 deletions apps/website/emails/drip-render-followup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export function dripRenderFollowupHtml(day: number): { subject: string; html: st

if (day === 10) {
return {
subject: 'The pilot program includes hands-on integration',
subject: 'An optional eight-week path from pilot to production',
html: wrapEmail({
body: `
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#1a7a40;font-weight:700;margin:0 0 8px">Pilot Program</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(26,122,64,0.08)">
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#1a7a40">Week 1</strong> · Integration &amp; first stream</p>
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#1a7a40">Month 1</strong> · First agent in staging</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#1a7a40">Month 3</strong> · Production deployment</p>
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#1a7a40">Week 8</strong> · Production readiness</p>
</div>
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#1a7a40;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
`,
Expand Down
Loading
Loading