Compare commits

..

53 Commits

Author SHA1 Message Date
will.anderson 20198bf345 Retire family/child Stripe tier; reframe free tier as 14-day trial
The 18+-only model drops the family/child plan entirely, so its
Stripe wiring is dead code. Remove the /api/family/{invite,members,remove}
routes, the STRIPE_PRICE_FAMILY_CHILD env/state config, the account-page
Family members UI and its dashboard JS, and the "Family accounts" roadmap
bullet. Free/Pro/Founding paths are untouched.

Marketing/pricing copy still promised a permanent free tier, which no
longer matches the product. Reframe the free-forever language as a
14-day free trial across the pricing card, comparison table, hero/SEO
meta, checkout page copy, and the no-ads promise. Prices and checkout
mechanics are unchanged — copy only.
2026-07-21 10:21:46 -05:00
will.anderson b87b0bed24 Merge pull request 'fix(web): soften two unbacked claims for closed-beta honesty' (#157) from fix/honest-claims-closed-beta into main
Deploy marketing to Cloud Run / deploy (push) Failing after 3m33s
2026-06-18 16:40:41 +00:00
Tim Lingo ef352d2b8a fix(web): soften two false claims for closed-beta honesty
Audit found two website claims with no backing implementation:
- '3 marketplace plugins included' (free tier) -> 'Plugin marketplace access
  (coming soon)'. The marketplace is not built; there are no free plugins.
- 'Connectors - day one' -> 'Connectors (rolling out)'. Only a few connectors
  are available; Slack is coming-soon, Gmail/Drive not yet shipped.

String-content only; elc syntax-checks clean. Three other audit flags (no-card
wording, crisis-line copy, post-quantum 'everything encrypted') intentionally
NOT touched - left for human review (sensitive / conflicting evidence).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:42:51 -05:00
will.anderson bb690c4690 Merge pull request 'Deploy: clean prod build — strip CGI content' (#112) from stage into main
Deploy marketing to Cloud Run / deploy (push) Failing after 41s
2026-05-11 21:49:47 +00:00
will.anderson e148e6987d Merge pull request 'Force full El rebuild — strip CGI content' (#111) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 7m44s
2026-05-11 21:43:20 +00:00
will.anderson 5885826316 Merge pull request 'Deploy: CI fixes (migrations, source guard, asset-only detection)' (#109) from stage into main
Deploy marketing to Cloud Run / deploy (push) Successful in 1m47s
2026-05-11 20:59:04 +00:00
will.anderson 9554430b7e Merge pull request 'Also skip El rebuild for workflow-only changes' (#108) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 8m23s
2026-05-11 20:47:04 +00:00
will.anderson 9685a42c7d Merge pull request 'Skip El rebuild for migration/script/test-only changes' (#106) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 11m48s
2026-05-11 20:45:32 +00:00
will.anderson cac986c5e1 Merge pull request 'promote: stage → main' (#5) from stage into main
Deploy marketing to Cloud Run / deploy (push) Failing after 30s
2026-05-11 20:31:39 +00:00
will.anderson 9650dad951 Merge pull request 'Update CORS test: no-Origin requests are allowed' (#104) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 8m44s
2026-05-11 20:22:39 +00:00
will.anderson c3aec8947a Merge pull request 'Fix stage source guard: fetch origin/dev before ancestry check' (#102) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 9m28s
2026-05-11 19:09:52 +00:00
will.anderson 441d6d7cb5 Fix: idempotent migration policy creation
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 12s
2026-05-11 18:56:50 +00:00
will.anderson de9bf25437 Merge pull request 'dev → stage: fix CI migration heredoc YAML parse error' (#98) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 23s
Merge PR #98 from dev into stage
2026-05-11 18:34:15 +00:00
will.anderson a59fdf4baa Merge pull request 'dev → stage: fix supabase-config null-origin CORS bug' (#96) from dev into stage
Merge PR #96 from dev into stage
2026-05-11 18:31:00 +00:00
will.anderson ae633d3f71 Merge pull request 'dev → stage: wire Supabase migrations into CI/CD' (#94) from dev into stage
Merge PR #94 from dev into stage
2026-05-11 18:22:17 +00:00
will.anderson 43b5286fd5 Merge pull request 'dev → stage: pricing buttons, API keys, enterprise contacts' (#92) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 7m55s
Merge dev into stage
2026-05-11 18:06:02 +00:00
will.anderson 04641ed1a3 Merge pull request 'Stage: CI fixes, pricing buttons, API key provisioning' (#90) from fix/stage-ci-paths into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 10s
2026-05-11 17:49:37 +00:00
will.anderson f4a202e220 Merge pull request 'dev → stage: CSP unsafe-eval fix' (#89) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 7m59s
Merge dev into stage
2026-05-11 16:45:12 +00:00
will.anderson 3482e7e0f5 Merge pull request 'dev → stage: remove --obfuscate (CSP/eval fix)' (#87) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 2m31s
2026-05-11 16:11:58 +00:00
will.anderson beee0f99a7 Merge pull request 'Stage: fix Stripe CDN mock + free-plan waitForLoadState sync' (#85) from dev into stage
Stage: fix Stripe CDN mock + free-plan waitForLoadState sync
2026-05-11 15:36:35 +00:00
will.anderson 4b70e8c186 Merge pull request 'Fix Stripe CDN mock override and free-plan sync guards in E2E tests' (#83) from fix/stage-ci-paths into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 11s
Fix Stripe CDN mock override and free-plan sync guards in E2E tests
2026-05-11 14:55:22 +00:00
will.anderson f9a5f93070 Merge pull request 'Stage: fix CI JS corruption from obfuscator stdout + flaky test guards' (#82) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 20m46s
Stage: fix CI JS corruption + flaky test guards
2026-05-11 14:16:48 +00:00
will.anderson 8e2deab5cb Merge pull request 'Stage: free plan age verification + soul demo personalization' (#80) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 23m41s
Stage: free plan age verification + soul demo personalization
2026-05-11 07:05:52 +00:00
will.anderson ddeca2250e Merge pull request 'dev → stage: CI paths + comprehensive checkout tests' (#78) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 20m10s
2026-05-11 06:21:21 +00:00
will.anderson d228701828 Merge pull request 'dev → stage: comprehensive checkout + Stripe tests' (#76) from dev into stage
Merge: dev into stage — comprehensive checkout + Stripe tests
2026-05-11 06:19:33 +00:00
will.anderson 41f27e83aa Merge pull request 'test: full Playwright + API test suite for stage' (#74) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 6m43s
Merge dev into stage
2026-05-11 05:29:33 +00:00
will.anderson 533436e2c2 Merge pull request 'security: pentest fixes — deploy to stage' (#70) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m32s
security: pentest fixes — deploy to stage
2026-05-11 04:57:20 +00:00
will.anderson aeea037e6f Merge pull request 'feat: auth-gate demo chat + budget circuit breaker' (#68) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m53s
feat: auth-gate demo chat + budget circuit breaker
2026-05-11 04:45:56 +00:00
will.anderson 41bad94368 Merge pull request 'feat: scale fixes — max-instances, asset caching, shared rate limits, global cap' (#66) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 4m15s
feat: scale fixes — max-instances, asset caching, shared rate limits, global cap
2026-05-11 03:12:54 +00:00
will.anderson 3020b4e902 Merge pull request 'feat: extract soul-demo into standalone Cloud Run service' (#64) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 4m5s
feat: extract soul-demo into standalone Cloud Run service
2026-05-11 02:09:27 +00:00
will.anderson e82425a829 Merge pull request 'deploy: fix HAVE_CURL verification' (#62) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
2026-05-11 01:07:49 +00:00
will.anderson c4cdb31529 Merge pull request 'deploy: fix HAVE_CURL — enable chat proxy to soul-demo' (#60) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 57s
deploy: fix HAVE_CURL — enable chat proxy to soul-demo
2026-05-11 01:03:41 +00:00
will.anderson a1c0cc090d Merge pull request 'Deploy: replace k3s with direct soul-demo watchdog' (#58) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m54s
Merge dev into stage
2026-05-11 00:47:16 +00:00
will.anderson 7df96a2273 Merge pull request 'Deploy: fix envelope truncation' (#56) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m50s
2026-05-11 00:23:38 +00:00
will.anderson d3b890b739 Merge pull request 'Deploy: fix JS served as JSON envelope' (#54) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m11s
Deploy: fix JS served as JSON envelope
2026-05-10 22:34:57 +00:00
will.anderson 3f069eeb79 Merge pull request 'Fix checkout auth (dev → stage)' (#52) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
Fix checkout auth (dev → stage)
2026-05-10 22:01:17 +00:00
will.anderson 8676751ed6 Merge pull request 'Fix http handler registration (dev → stage)' (#50) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m40s
Merge PR #50: Fix http handler registration (dev → stage)
2026-05-10 18:37:16 +00:00
will.anderson a4f5312069 Merge pull request 'Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04' (#48) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m37s
Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
2026-05-10 18:02:14 +00:00
will.anderson c76e5a19eb Merge pull request 'Non-blocking entrypoint + k3s --flannel-iface fix' (#46) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m8s
Non-blocking entrypoint + k3s flannel-iface fix
2026-05-10 17:55:12 +00:00
will.anderson 58b7b32cdd Single-stage Dockerfile.stage: pre-download k3s on host runner
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 12s
2026-05-10 16:27:07 +00:00
will.anderson 0fdabcce86 Merge pull request 'promote: dev → stage' (#42) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m46s
promote: dev → stage
2026-05-10 15:57:36 +00:00
will.anderson 79de47de2c Merge pull request 'promote: dev → stage' (#40) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 10m59s
2026-05-10 02:26:35 +00:00
will.anderson 45963154d9 Merge pull request 'promote: dev → stage' (#35) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m42s
2026-05-10 01:32:52 +00:00
will.anderson aabaa2ffb0 Merge pull request 'promote: dev → stage' (#33) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 15m11s
2026-05-10 01:07:20 +00:00
will.anderson d5dcb08ec6 Merge pull request 'promote: dev → stage (soul-demo image tar fix)' (#31) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 24s
2026-05-10 01:01:01 +00:00
will.anderson 20a36eeb9e Merge pull request 'promote: dev → stage' (#29) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 3m11s
2026-05-10 00:34:47 +00:00
will.anderson 32a179c24a Merge pull request 'promote: dev → stage' (#27) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-10 00:12:47 +00:00
will.anderson 6bc026de19 Merge pull request 'promote: dev → stage' (#25) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 21s
2026-05-09 23:44:30 +00:00
will.anderson 0ae526b72e Merge pull request 'promote: dev → stage' (#23) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 22s
2026-05-09 23:35:56 +00:00
will.anderson 8221aef605 promote: dev → stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-09 18:34:59 +00:00
will.anderson f8487c43a0 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m16s
2026-05-09 17:41:09 +00:00
will.anderson 36b99dd9e2 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 6s
2026-05-09 17:32:23 +00:00
will.anderson cd93af38fb Merge pull request 'promote: stage → main' (#5) from stage into main 2026-05-05 11:07:25 +00:00
20 changed files with 106 additions and 567 deletions
+1 -6
View File
@@ -82,12 +82,7 @@ jobs:
echo "Changed files:"
echo "$CHANGED"
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$CHANGED" ] || [ "$CHANGED" = "unknown" ]; then
# No diff (workflow_dispatch with no new commits, or git error).
# Registry may not have a stage-latest base image, so force full build.
echo "asset_only=false" >> "$GITHUB_OUTPUT"
echo "=> No changed files detected (workflow_dispatch?), forcing full build"
elif [ -z "$NON_ASSET" ]; then
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
else
-1
View File
@@ -9,7 +9,6 @@
#
# CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing
# Last rebuilt: 2026-05-11
FROM ubuntu:24.04
+20 -21
View File
@@ -1,4 +1,3 @@
// elhtml_impl.c — El HTML element stubs.
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
@@ -20,11 +19,11 @@ el_val_t el_li(el_val_t attrs, el_val_t children);
el_val_t el_p(el_val_t attrs, el_val_t children);
el_val_t el_span(el_val_t attrs, el_val_t children);
el_val_t el_form(el_val_t attrs, el_val_t children);
el_val_t el_h1(el_val_t attrs, el_val_t children);
el_val_t el_h2(el_val_t attrs, el_val_t children);
el_val_t el_h3(el_val_t attrs, el_val_t children);
el_val_t el_h4(el_val_t attrs, el_val_t children);
el_val_t el_button(el_val_t attrs, el_val_t children);
el_val_t el_h1(el_val_t attrs, el_val_t text);
el_val_t el_h2(el_val_t attrs, el_val_t text);
el_val_t el_h3(el_val_t attrs, el_val_t text);
el_val_t el_h4(el_val_t attrs, el_val_t text);
el_val_t el_button(el_val_t attrs, el_val_t label);
el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children);
el_val_t el_input(el_val_t type_attr, el_val_t attrs);
el_val_t el_textarea(el_val_t attrs, el_val_t value);
@@ -177,43 +176,43 @@ el_val_t el_form(el_val_t attrs, el_val_t children) {
return 0;
}
el_val_t el_h1(el_val_t attrs, el_val_t children) {
el_val_t el_h1(el_val_t attrs, el_val_t text) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h1>"), children), EL_STR("</h1>"));
return el_str_concat(el_str_concat(EL_STR("<h1>"), el_escape(text)), EL_STR("</h1>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h1 "), attrs), EL_STR(">")), children), EL_STR("</h1>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h1 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h1>"));
return 0;
}
el_val_t el_h2(el_val_t attrs, el_val_t children) {
el_val_t el_h2(el_val_t attrs, el_val_t text) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h2>"), children), EL_STR("</h2>"));
return el_str_concat(el_str_concat(EL_STR("<h2>"), el_escape(text)), EL_STR("</h2>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h2 "), attrs), EL_STR(">")), children), EL_STR("</h2>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h2 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h2>"));
return 0;
}
el_val_t el_h3(el_val_t attrs, el_val_t children) {
el_val_t el_h3(el_val_t attrs, el_val_t text) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h3>"), children), EL_STR("</h3>"));
return el_str_concat(el_str_concat(EL_STR("<h3>"), el_escape(text)), EL_STR("</h3>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h3 "), attrs), EL_STR(">")), children), EL_STR("</h3>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h3 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h3>"));
return 0;
}
el_val_t el_h4(el_val_t attrs, el_val_t children) {
el_val_t el_h4(el_val_t attrs, el_val_t text) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h4>"), children), EL_STR("</h4>"));
return el_str_concat(el_str_concat(EL_STR("<h4>"), el_escape(text)), EL_STR("</h4>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h4 "), attrs), EL_STR(">")), children), EL_STR("</h4>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h4 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h4>"));
return 0;
}
el_val_t el_button(el_val_t attrs, el_val_t children) {
el_val_t el_button(el_val_t attrs, el_val_t label) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<button type=\"button\">"), children), EL_STR("</button>"));
return el_str_concat(el_str_concat(EL_STR("<button type=\"button\">"), el_escape(label)), EL_STR("</button>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<button type=\"button\" "), attrs), EL_STR(">")), children), EL_STR("</button>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<button type=\"button\" "), attrs), EL_STR(">")), el_escape(label)), EL_STR("</button>"));
return 0;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
-6
View File
@@ -1764,7 +1764,6 @@ el_val_t page_css(void) {
"\n"
" #neuron-demo-input-row {\n"
" display: flex;\n"
" align-items: flex-end;\n"
" border-top: 1px solid var(--border);\n"
" flex-shrink: 0;\n"
" }\n"
@@ -1772,16 +1771,11 @@ el_val_t page_css(void) {
" flex: 1;\n"
" font-family: var(--body);\n"
" font-size: 0.875rem;\n"
" line-height: 1.5;\n"
" color: var(--t1);\n"
" background: var(--bg);\n"
" border: none;\n"
" outline: none;\n"
" padding: 0.875rem 1rem;\n"
" resize: none;\n"
" overflow: hidden;\n"
" min-height: 2.75rem;\n"
" max-height: 7.5rem;\n"
" }\n"
" #neuron-demo-text::placeholder { color: var(--t3); }\n"
" #neuron-demo-send {\n"
+1 -1
View File
@@ -705,7 +705,7 @@ el_val_t handle_chat(el_val_t body) {
el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; });
el_val_t user_name = json_get(body, EL_STR("user_name"));
el_val_t name_line = ({ el_val_t _tmp = 0; if (str_eq(user_name, EL_STR(""))) { _tmp = EL_STR(""); } else { _tmp = el_str_concat(el_str_concat(EL_STR(" The visitor's name is "), user_name), EL_STR(". Use their first name in the greeting.")); } _tmp; });
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron \xe2\x80\x94 a personal AI. Someone just opened the chat. Say hello.\n\nWarm and casual. Like you're genuinely glad they showed up. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. One is often right.\n- No markdown, no headers, no bullets.\n- No mention of the question limit \xe2\x80\x94 the UI shows that.\n- No retail-AI phrases like 'How can I help you today?'\n- No lines about being alive, sentient, or experiencing anything. Just be present and friendly.\n- If you know their name, use it once, naturally.\n- Ask how they're doing, or what's on their mind. Keep it simple.\n\nGo."));
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- If you know their name, use it once, naturally. Don't make a big deal of it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me."));
el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\"));
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
-49
View File
@@ -237,55 +237,6 @@ el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_va
return http_post_with_headers(EL_STR(url), body_json, headers);
}
/*
* supabase_admin_update_user PUT {project_url}/auth/v1/admin/users/{user_id}
* with the service-role key to overwrite a user's user_metadata (and any other
* top-level fields in body_json). Unlike /auth/v1/invite, this always writes
* the supplied data even when the user already exists.
*
* body_json example:
* {"user_metadata":{"plan":"founding","stripe_customer_id":"cus_xxx","name":"..."}}
*
* Returns the raw JSON response from Supabase (includes the updated user object).
* Returns "" on transport error.
*
* Used by the Stripe webhook after supabase_admin_invite to guarantee the
* plan is stamped correctly regardless of whether the account was created
* before or after payment.
*/
el_val_t supabase_admin_update_user(el_val_t project_url, el_val_t service_key,
el_val_t user_id, el_val_t body_json) {
CURL *c = curl_easy_init();
if (!c) return EL_STR("");
char url[1024];
snprintf(url, sizeof(url), "%s/auth/v1/admin/users/%s",
EL_CSTR(project_url), EL_CSTR(user_id));
char auth_hdr[2048];
snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", EL_CSTR(service_key));
char api_hdr[2048];
snprintf(api_hdr, sizeof(api_hdr), "apikey: %s", EL_CSTR(service_key));
struct curl_slist *hdrs = NULL;
hdrs = curl_slist_append(hdrs, auth_hdr);
hdrs = curl_slist_append(hdrs, api_hdr);
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
hdrs = curl_slist_append(hdrs, "Accept: application/json");
_stub_resp_t r = {0};
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT");
curl_easy_setopt(c, CURLOPT_POSTFIELDS, EL_CSTR(body_json));
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, _stub_write);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &r);
CURLcode rc = curl_easy_perform(c);
curl_easy_cleanup(c);
curl_slist_free_all(hdrs);
if (rc != CURLE_OK) { free(r.buf); return EL_STR(""); }
if (!r.buf) return EL_STR("");
return EL_STR(r.buf);
}
/*
* gcs_get_token fetch an OAuth2 bearer token.
*
+2 -63
View File
@@ -77,23 +77,6 @@ static _Thread_local int _tl_arena_active = 0;
* Allows serving PNGs and other binary files without strlen truncation. */
static _Thread_local size_t _tl_fs_read_len = 0;
/* Binary body side-channel for http_response().
*
* http_response() normally JSON-encodes the body via jb_emit_escaped(), which
* stops at the first null byte (C-string semantics). Binary files like PNGs
* contain null bytes as early as byte 8 (IHDR chunk length), causing truncation.
*
* When _tl_fs_read_len > 0 at the time http_response() is called, we skip
* JSON-encoding and instead:
* 1. malloc-copy the raw bytes here
* 2. write the sentinel string "__el_binary__" into the envelope body field
* 3. In http_send_response(), detect the sentinel and use these raw bytes
*
* Thread-local so each worker thread has independent storage.
* Lifecycle: set by http_response(), consumed (and freed) by http_send_response(). */
static _Thread_local char* _tl_binary_body = NULL;
static _Thread_local size_t _tl_binary_size = 0;
static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return;
if (_tl_arena.count >= _tl_arena.cap) {
@@ -1553,22 +1536,10 @@ static void http_send_response(int fd, const char* body) {
}
const char* eff_body = is_envelope ? env_body : body;
int binary_side_channel = 0;
/* Binary side-channel: if the envelope body is the sentinel "__el_binary__",
* http_response() stored the real bytes in _tl_binary_body/_tl_binary_size.
* Substitute them here so http_send_all() sends the correct binary payload. */
if (is_envelope && env_body && strcmp(env_body, "__el_binary__") == 0
&& _tl_binary_body && _tl_binary_size > 0) {
eff_body = _tl_binary_body;
binary_side_channel = 1;
}
/* Use the real byte count from fs_read if available (handles binary files
* with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for
* normal text/JSON responses where _tl_fs_read_len is 0. */
size_t blen = binary_side_channel ? _tl_binary_size
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */
int head_only = _tl_http_head_only;
@@ -1616,13 +1587,6 @@ static void http_send_response(int fd, const char* body) {
if (env_parsed_root) el_release(env_parsed_root);
free(env_body);
free(hdrs.buf);
/* Release binary side-channel if it was used (or left over from an error). */
if (_tl_binary_body) {
free(_tl_binary_body);
_tl_binary_body = NULL;
_tl_binary_size = 0;
}
}
typedef struct {
@@ -1997,14 +1961,6 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
const char* b = EL_CSTR(body);
if (!b) b = "";
/* Capture binary length BEFORE clearing _tl_fs_read_len.
* If the body came from fs_read(), _tl_fs_read_len holds the real byte
* count. jb_emit_escaped() stops at the first NUL byte, so we cannot
* JSON-encode binary data directly. Instead we copy it to a thread-local
* side-channel and write the sentinel "__el_binary__" into the envelope.
* http_send_response() detects the sentinel and uses the side-channel. */
size_t binary_len = _tl_fs_read_len;
/* Clear the fs_read binary-length hint: the envelope we're about to build
* is a fresh JSON string, not the raw file bytes. Without this reset,
* http_worker would use the stale _tl_fs_read_len (= original file size)
@@ -2012,18 +1968,6 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
* http_send_response and http_parse_envelope. */
_tl_fs_read_len = 0;
if (binary_len > 0) {
/* Binary body path: store raw bytes in thread-local, emit sentinel. */
free(_tl_binary_body); /* discard any stale binary from a prior error path */
_tl_binary_body = malloc(binary_len);
if (_tl_binary_body) {
memcpy(_tl_binary_body, b, binary_len);
_tl_binary_size = binary_len;
} else {
_tl_binary_size = 0; /* malloc failed — fall through to empty body */
}
}
JsonBuf out; jb_init(&out);
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
jb_puts(&out, ",\"status\":");
@@ -2033,12 +1977,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
jb_puts(&out, ",\"headers\":");
jb_puts(&out, hj);
jb_puts(&out, ",\"body\":");
if (binary_len > 0 && _tl_binary_body) {
/* Sentinel: http_send_response() will substitute the real bytes. */
jb_puts(&out, "\"__el_binary__\"");
} else {
jb_emit_escaped(&out, b);
}
jb_emit_escaped(&out, b);
jb_putc(&out, '}');
return el_wrap_str(out.buf);
}
+1 -30
View File
@@ -651,8 +651,7 @@ fn account_roadmap_phase2() -> String {
el_li("", "Process and knowledge packets") +
el_li("", "Local inference via Ollama") +
el_li("", "Additional connectors &mdash; GitHub, Notion, Linear, more") +
el_li("", "Mobile companion app") +
el_li("", "Family accounts")
el_li("", "Mobile companion app")
)
)
}
@@ -692,33 +691,6 @@ fn account_roadmap_section() -> String {
)
}
fn account_family_section() -> String {
let child_attest: String = "<label style=\"display:flex;gap:.5rem;align-items:flex-start;cursor:pointer;margin:.75rem 0\">" +
"<input type=\"checkbox\" id=\"family-attest\" style=\"margin-top:.2rem;accent-color:var(--navy)\">" +
"<span style=\"font-size:.8rem;color:var(--t2)\">I confirm this person is a minor under my care and is using this account under my supervision.</span>" +
"</label>"
el_div(
"id=\"family-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\"",
el_div(
"class=\"acct-section-header\"",
el_p("class=\"card-label\"", "Family members") +
el_p("style=\"font-size:.8125rem;font-weight:300;color:var(--t2);line-height:1.65\"", "Add up to 5 children to your Founding Member plan. $10/month per child.")
) +
el_div("id=\"family-list\"", "") +
el_div(
"id=\"add-child-form\"",
el_input("email", "id=\"child-email\" placeholder=\"Child&#39;s email\" class=\"acct-input\"") +
el_input("number", "id=\"child-dob-year\" placeholder=\"Birth year (e.g. 2012)\" min=\"1990\" max=\"2007\" class=\"acct-input\"") +
child_attest +
el_button("class=\"btn-primary\" onclick=\"addFamilyMember()\" style=\"padding:.75rem 1.5rem\"", "Add family member") +
el_p("id=\"family-msg\" style=\"display:none;font-size:.8rem;margin-top:.5rem\"", "")
)
)
)
}
fn account_badge_section() -> String {
el_div(
"id=\"badge-section\" style=\"display:none\"",
@@ -952,7 +924,6 @@ fn account_dashboard_section() -> String {
account_plan_card() +
account_api_keys_section() +
account_roadmap_section() +
account_family_section() +
account_badge_section() +
account_timeline_card() +
account_download_card() +
+3 -27
View File
@@ -98,7 +98,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
el_li("", "Persistent memory - never resets")
+ el_li("", "Local inference via Ollama (coming)")
+ el_li("", "Bring your own API keys")
+ el_li("", "3 marketplace plugins included")
+ el_li("", "Plugin marketplace access (coming soon)")
+ el_li("", "Core built-in capabilities")
+ el_li("", "2 devices included")
} else {
@@ -341,35 +341,11 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
let free_init_script: String = ""
return nav_html + main_html + supabase_script + stripe_script + style_html + stripe_el_script + cfg_script + auth_script + free_init_script
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
}
fn checkout_style_html() -> String {
let css: String = ".checkout-shell {
max-width: 980px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: start;
}
.checkout-summary {
position: sticky;
top: 2rem;
}
.checkout-form-wrap {
min-width: 0;
}
@media (max-width: 860px) {
.checkout-shell {
grid-template-columns: 1fr;
gap: 2.5rem;
}
.checkout-summary {
position: static;
}
}
.checkout-plan-name {
let css: String = ".checkout-plan-name {
font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
+2 -2
View File
@@ -129,8 +129,8 @@ fn comparison_rows() -> String {
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$30/mo</span><br><span style=\"font-size:0.7rem\">Microsoft 365</span></td>" +
"</tr>"
let row10: String = "<tr>" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free tier</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, forever</span></td>" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free trial</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, 14 days</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">GPT-3.5 only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
+2 -71
View File
@@ -1,5 +1,5 @@
// account-dashboard.el -- Account dashboard: session check, plan card, family.
// Handles onAuthStateChange, plan card rendering, family member management.
// account-dashboard.el -- Account dashboard: session check, plan card.
// Handles onAuthStateChange and plan card rendering.
// Compiled with: elc --target=js --bundle --minify --obfuscate
//
// Required globals: window.NEURON_CFG.supabase_url, window.NEURON_CFG.supabase_anon_key
@@ -127,77 +127,8 @@ fn main() -> Void {
var roadmapSection = document.getElementById('roadmap-section');
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
if (plan === 'founding') {
var famSection = document.getElementById('family-section');
if (famSection) famSection.style.display = 'block';
var session = await sb.auth.getSession();
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
if (userEmail) loadFamilyMembers(userEmail);
}
}
async function loadFamilyMembers(parentEmail) {
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
var members = await r.json();
var list = document.getElementById('family-list');
if (!list) return;
if (!members || !members.length) {
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
return;
}
list.innerHTML = members.map(function(m) {
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">'
+ '<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>'
+ '<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>'
+ '<button onclick=\"removeFamilyMember(\'' + m.child_email + '\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>'
+ '</div>';
}).join('');
}
window.addFamilyMember = async function() {
var email = document.getElementById('child-email').value.trim();
var year = document.getElementById('child-dob-year').value;
var attest = document.getElementById('family-attest').checked;
var msg = document.getElementById('family-msg');
if (!email || !year || !attest) {
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Please fill in all fields and confirm the attestation.'; return;
}
if (parseInt(year) < 2008) {
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Child must be under 18. Birth year must be 2008 or later.'; return;
}
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
var r = await fetch('/api/family/invite', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
});
var d = await r.json();
msg.style.display = 'block';
if (d.ok) {
msg.style.color = 'var(--navy)';
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
document.getElementById('child-email').value = '';
document.getElementById('child-dob-year').value = '';
document.getElementById('family-attest').checked = false;
loadFamilyMembers(parentEmail);
} else {
msg.style.color = '#c44';
msg.textContent = d.error || 'Something went wrong.';
}
};
window.removeFamilyMember = async function(childEmail) {
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
await fetch('/api/family/remove', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
});
loadFamilyMembers(parentEmail);
};
function renderUserChip(user) {
var email = user.email || '';
var avatarEl = document.getElementById('user-avatar-el');
+21 -45
View File
@@ -142,27 +142,14 @@ fn main() -> Void {
}
}
function _todayUTC() { return Math.floor(Date.now() / 86400000); }
function loadSession() {
try {
var s = localStorage.getItem('neuron_demo_session');
var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
// Reset count (and conversation) on new UTC day keeps client in sync with server
var today = _todayUTC();
if (parsed.day !== today) {
parsed.count = 0;
parsed.messages = [];
parsed.greeted = false;
parsed.day = today;
}
return parsed;
return s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
} catch(e) { return { messages: [], count: 0, context: '' }; }
}
function saveSession(session) {
try {
if (!session.day) session.day = _todayUTC();
localStorage.setItem('neuron_demo_session', JSON.stringify(session));
} catch(e) {}
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
}
function clearSession() {
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
@@ -385,14 +372,7 @@ fn main() -> Void {
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
updateCountdown();
// Replay existing history if present; only greet fresh sessions
if (session.messages && session.messages.length > 0) {
if (msgs && msgs.children.length === 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
}
} else {
_sendIntroGreeting();
}
_sendIntroGreeting();
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
@@ -497,7 +477,6 @@ fn main() -> Void {
return;
}
input.value = '';
input.style.height = 'auto';
btn.disabled = true;
addMsg('user', msg);
@@ -546,33 +525,34 @@ fn main() -> Void {
// Server-side rate limit show a live countdown to reset
if (d.rate_limited && d.reset_at) {
var _showRateTimer = function() {
var now = Math.floor(Date.now() / 1000);
var now = Math.floor(Date.now() / 1000);
var secsLeft = Math.max(0, d.reset_at - now);
var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return \"You've reached today's limit. Resets in \" + hh + ':' + pad(mm) + ':' + pad(ss) + '.';
var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
return \"You've had 10 conversations today. Come back in \" + ts + \".\";
};
addMsg('ai', _showRateTimer());
// Update the bubble text with a live ticker
// Update the last ai message with a live ticker
var _timerInterval = setInterval(function() {
var msgsEl = document.getElementById('neuron-demo-messages');
if (!msgsEl) { clearInterval(_timerInterval); return; }
var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai');
var lastAi = aiMsgs[aiMsgs.length - 1];
var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null;
if (lastBubble) { lastBubble.textContent = _showRateTimer(); }
var thMsgsInner = document.getElementById('neuron-demo-msgs');
if (!thMsgsInner) { clearInterval(_timerInterval); return; }
var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
var lastAi = aiMsgs[aiMsgs.length - 1];
if (lastAi) { lastAi.textContent = _showRateTimer(); }
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
clearInterval(_timerInterval);
if (lastBubble) { lastBubble.textContent = \"You're all set conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; }
msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown();
if (lastAi) { lastAi.textContent = \"You're all set conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; }
}
}, 1000);
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
if (btn) { btn.disabled = true; }
if (btn) { btn.disabled = false; }
if (input) { input.focus(); }
return;
}
@@ -618,10 +598,6 @@ fn main() -> Void {
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); window.neuronDemoSend(); }
});
inp.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
}
})()")
}
+17 -3
View File
@@ -1,6 +1,20 @@
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age
// verification. This file is no longer compiled or loaded.
// checkout-free.el -- Free plan: show success panel after auth completes.
// Watches the auth-badge element; when it becomes visible, hides the auth
// section and shows the free-success panel. No card required for free tier.
// Compiled with: elc --target=js --bundle --minify --obfuscate
fn main() -> Void {
native_js("(function() {
var success = document.getElementById('free-success');
var auth = document.getElementById('auth-section');
if (!success) return;
var timer = setInterval(function() {
var badge = document.getElementById('auth-badge');
if (badge && badge.offsetParent !== null) {
if (auth) auth.style.display = 'none';
success.style.display = '';
clearInterval(timer);
}
}, 150);
})()")
}
+3 -3
View File
@@ -31,8 +31,8 @@ fn main() -> Void {
if (spinner) spinner.style.display = loading ? '' : 'none';
}
// Free plan: Stripe SetupIntent for age verification (card saved, never charged).
// Falls through to the same Stripe init path server returns setup_mode=true for free.
// Free plan has no payment form bail out entirely.
if (str_eq(PLAN, 'free')) return;
window._neuronMode = 'payment';
var paymentEl = null;
@@ -101,7 +101,7 @@ fn main() -> Void {
if (submitLabel) {
submitLabel.textContent = window._neuronMode === 'setup'
? 'Save my card - no charge today '
: PLAN === 'free' ? 'Verify age & get started ' : 'Complete purchase ';
: 'Complete purchase ';
}
waitForStripe(function() {
if (!stripe) stripe = Stripe(STRIPE_PK);
+1 -1
View File
@@ -55,7 +55,7 @@ fn local_first() -> String {
let principle3: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:240ms\"",
el_p("class=\"lf-principle-label\"", "No ads. Ever.") +
el_p("class=\"lf-principle-body\"", "Not on the free tier. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.")
el_p("class=\"lf-principle-body\"", "Not during your trial. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.")
)
let principle4: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:360ms\"",
+20 -226
View File
@@ -415,10 +415,8 @@ fn waitlist_upsert(email: String, name: String, plan: String, source: String, at
let ua_safe: String = str_replace(str_replace(user_agent, "\\", "\\\\"), "\"", "\\\"")
let num_field: String = if member_num > 0 { ",\"member_number\":" + int_to_str(member_num) } else { "" }
let row: String = "{\"email\":\"" + e_safe + "\",\"name\":\"" + n_safe + "\",\"plan\":\"" + plan + "\",\"source\":\"" + source + "\",\"attestation\":\"" + a_safe + "\",\"user_agent\":\"" + ua_safe + "\"" + num_field + "}"
// Use on_conflict=email,plan so existing rows are updated (upsert)
// rather than silently failing on duplicate key.
let resp: String = supabase_insert(sb_url, sb_key, "waitlist?on_conflict=email,plan", row)
println("[waitlist] supabase upsert -> " + resp)
let resp: String = supabase_insert(sb_url, sb_key, "waitlist", row)
println("[waitlist] supabase insert -> " + resp)
return ""
}
@@ -630,7 +628,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"Founding Member Checkout — Neuron"
} else {
if str_eq(plan, "free") {
"Get Started Free — Neuron"
"Start Your Free Trial — Neuron"
} else {
"Professional Plan Checkout — Neuron"
}
@@ -639,7 +637,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
} else {
if str_eq(plan, "free") {
"Create your free Neuron account. A card verifies you're 18+ — you won't be charged. Your AI that remembers you, runs on your machine, never resets."
"Start your 14-day free trial. No credit card required. Your AI that remembers you runs on your machine, never resets."
} else {
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
}
@@ -699,21 +697,23 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
}
// Free tier: $0 PaymentIntent for age verification (18+ requirement).
// Verifies card is valid. No charge, no capture.
// Note: setup_future_usage cannot be used with amount=0.
// Free tier: creates a SetupIntent for age verification (18+ requirement).
// No charge but the user must provide a valid payment method.
if str_eq(plan, "free") {
let free_pi_body: String = "amount=0"
+ "&currency=usd"
+ "&payment_method_types[]=card"
let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification"
let free_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
let free_pi_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/payment_intents",
free_pi_body,
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
let free_si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
free_si_body,
auth_header)
return free_pi_resp
if str_starts_with(free_si_resp, "{") {
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return free_si_resp
}
// Setup-mode path: save payment method, do not charge. Only valid
@@ -875,32 +875,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return "{\"rows\":" + ac_resp + "}"
}
// Admin: reset all demo rate limits
// POST { "admin_token": "<NEURON_ADMIN_TOKEN>" }
// Deletes all rows from demo_rate_limits resets every user's daily quota.
if str_eq(path, "/api/admin/reset-rate-limits") {
if !str_eq(method, "POST") {
return "{\"__status__\":405,\"error\":\"POST required\"}"
}
let rrl_token_in: String = json_get(body, "admin_token")
let rrl_token_exp: String = env("NEURON_ADMIN_TOKEN")
if str_eq(rrl_token_exp, "") {
return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}"
}
if !str_eq(rrl_token_in, rrl_token_exp) {
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
}
let rrl_sb_url: String = state_get("__supabase_project_url__")
let rrl_sb_key: String = state_get("__supabase_service_key__")
if str_eq(rrl_sb_url, "") || str_eq(rrl_sb_key, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
// DELETE /rest/v1/demo_rate_limits?uid=not.is.null (all rows)
let rrl_url: String = rrl_sb_url + "/rest/v1/demo_rate_limits?uid=not.is.null"
let _rrl_resp: String = http_delete_auth(rrl_url, rrl_sb_key, rrl_sb_key)
return "{\"ok\":true,\"message\":\"rate limits cleared\"}"
}
// My plan: server-side waitlist read with JWT verification
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
// /auth/v1/user, extract the email, then read the waitlist row with the
@@ -1550,20 +1524,13 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
if is_session_done || is_pi_done || is_si_done {
// Pull email/name/customer_id - fields differ slightly across event
// types. Walk several candidates:
// receipt_email - PaymentIntent (founding one-time)
// data.object.* - full dot-path for checkout.session.completed (subscription)
// customer_details.email - substring fallback if nested key appears at any level
// billing_details.email - Elements payment intents
// types, walk a few candidates.
let customer_email: String = json_get(body, "receipt_email")
if str_eq(customer_email, "") { let customer_email = json_get(body, "data.object.customer_details.email") }
if str_eq(customer_email, "") { let customer_email = json_get(body, "customer_details.email") }
if str_eq(customer_email, "") { let customer_email = json_get(body, "billing_details.email") }
let customer_name: String = json_get(body, "data.object.customer_details.name")
if str_eq(customer_name, "") { let customer_name = json_get(body, "customer_details.name") }
let customer_name: String = json_get(body, "customer_details.name")
if str_eq(customer_name, "") { let customer_name = json_get(body, "billing_details.name") }
let customer_id: String = json_get(body, "data.object.customer")
if str_eq(customer_id, "") { let customer_id = json_get(body, "customer") }
let customer_id: String = json_get(body, "customer")
// Plan inference from metadata
let plan: String = "free"
@@ -1622,19 +1589,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
let _cust_resp: String = http_post_form_auth(cust_url, cust_body, stripe_auth)
}
}
// Always stamp user_metadata directly via Admin API.
// supabase_admin_invite re-sends a magic link for existing users
// but does NOT update their user_metadata so plan stays "free"
// for anyone who signed up (attestation, waitlist) before paying.
// This PUT is idempotent: safe for both new and returning users.
if !str_eq(new_user_id, "") {
let meta_body: String = "{\"user_metadata\":{\"plan\":\"" + plan_safe + "\""
+ ",\"name\":\"" + name_safe + "\""
+ ",\"stripe_customer_id\":\"" + cid_safe2 + "\""
+ ",\"email_verified\":true}}"
let _meta_resp: String = supabase_admin_update_user(wb_sb_url, wb_sb_key, new_user_id, meta_body)
println("[webhook] supabase user_metadata update for " + new_user_id + ": " + _meta_resp)
}
}
// 4. Forward to license API for key provisioning
@@ -2037,164 +1991,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return badge_css + badge_html
}
// Family plan POST /api/family/invite
if str_eq(path, "/api/family/invite") {
if !str_eq(method, "POST") {
return "{\"error\":\"POST required\"}"
}
let parent_email: String = json_get(body, "parent_email")
let child_email: String = json_get(body, "child_email")
let child_dob_year_str: String = json_get(body, "child_dob_year")
let attested_str: String = json_get(body, "attested")
if str_eq(parent_email, "") || str_eq(child_email, "") || str_eq(child_dob_year_str, "") {
return "{\"error\":\"parent_email, child_email, and child_dob_year are required\"}"
}
if !str_eq(attested_str, "true") {
return "{\"error\":\"attestation required\"}"
}
let child_dob_year: Int = str_to_int(child_dob_year_str)
if child_dob_year < 2008 {
return "{\"error\":\"Child must be under 18. Birth year must be 2008 or later.\"}"
}
// Check existing family member count
let fam_sb_url: String = state_get("__supabase_project_url__")
let fam_sb_key: String = state_get("__supabase_service_key__")
if str_eq(fam_sb_key, "") {
return "{\"error\":\"not configured\"}"
}
let pe_safe: String = str_replace(str_replace(parent_email, "\\", "\\\\"), "\"", "\\\"")
let ce_safe: String = str_replace(str_replace(child_email, "\\", "\\\\"), "\"", "\\\"")
let members_resp: String = http_get_auth(
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&status=neq.cancelled&select=child_email",
fam_sb_key
)
let member_count: Int = json_array_len(members_resp)
if member_count >= 5 {
return "{\"error\":\"Maximum of 5 family members reached.\"}"
}
// Check child not already added
let dup_resp: String = http_get_auth(
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&child_email=eq." + child_email + "&select=child_email",
fam_sb_key
)
let dup_count: Int = json_array_len(dup_resp)
if dup_count > 0 {
return "{\"error\":\"This child email is already in your family plan.\"}"
}
// Create Stripe customer for child
let fam_stripe_key: String = state_get("__stripe_secret_key__")
if str_eq(fam_stripe_key, "") {
return "{\"error\":\"Stripe not configured\"}"
}
let child_email_safe: String = str_replace(child_email, "@", "%40")
let customer_body: String = "email=" + child_email_safe
let customer_resp: String = http_post_auth("https://api.stripe.com/v1/customers", fam_stripe_key, customer_body)
let child_customer_id: String = json_get(customer_resp, "id")
if str_eq(child_customer_id, "") {
return "{\"error\":\"Failed to create Stripe customer\"}"
}
// Create Stripe subscription for child
let price_id: String = state_get("__stripe_price_family_child__")
if str_eq(price_id, "") {
return "{\"error\":\"Family child price not configured\"}"
}
let parent_email_safe: String = str_replace(parent_email, "@", "%40")
let sub_body: String = "customer=" + child_customer_id
+ "&items[0][price]=" + price_id
+ "&trial_period_days=7"
+ "&metadata[parent_email]=" + parent_email_safe
+ "&metadata[type]=family_child"
let sub_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions", fam_stripe_key, sub_body)
let sub_id: String = json_get(sub_resp, "id")
if str_eq(sub_id, "") {
return "{\"error\":\"Failed to create Stripe subscription\"}"
}
// Generate invite token
let ts_str: String = int_to_str(unix_timestamp())
let email_prefix: String = str_slice(child_email, 0, 4)
let invite_token: String = ts_str + email_prefix
// Insert into family_members table
let fam_row: String = "{\"parent_email\":\"" + pe_safe + "\",\"parent_user_id\":\"\",\"child_email\":\"" + ce_safe + "\",\"child_dob_year\":" + child_dob_year_str + ",\"status\":\"invited\",\"stripe_subscription_id\":\"" + sub_id + "\",\"invite_token\":\"" + invite_token + "\"}"
let fam_insert_resp: String = supabase_insert(fam_sb_url, fam_sb_key, "family_members", fam_row)
println("[family/invite] insert -> " + fam_insert_resp)
// Send invite email
let invite_text: String = "You have been invited to join Neuron by " + parent_email
+ ". Visit https://neurontechnologies.ai/account to set up your account."
let invite_send: String = send_email(
"Neuron <no-reply@neurontechnologies.ai>",
child_email,
"You have been invited to Neuron",
"",
invite_text
)
println("[family/invite] " + invite_send)
return "{\"ok\":true,\"invite_token\":\"" + invite_token + "\"}"
}
// Family plan GET /api/family/members
if str_starts_with(path, "/api/family/members") {
let fml_sb_url: String = state_get("__supabase_project_url__")
let fml_sb_key: String = state_get("__supabase_service_key__")
if str_eq(fml_sb_key, "") {
return "[]"
}
// Extract parent_email from query string
let fml_parent_email: String = ""
let q_idx: Int = str_index_of(path, "parent_email=")
if q_idx >= 0 {
let fml_parent_email = str_slice(path, q_idx + 13, str_len(path))
}
if str_eq(fml_parent_email, "") {
return "{\"error\":\"parent_email required\"}"
}
let fml_resp: String = http_get_auth(
fml_sb_url + "/rest/v1/family_members?parent_email=eq." + fml_parent_email + "&select=child_email,status,child_dob_year,created_at",
fml_sb_key
)
if str_eq(fml_resp, "") {
return "[]"
}
return fml_resp
}
// Family plan POST /api/family/remove
if str_eq(path, "/api/family/remove") {
if !str_eq(method, "POST") {
return "{\"error\":\"POST required\"}"
}
let rem_parent_email: String = json_get(body, "parent_email")
let rem_child_email: String = json_get(body, "child_email")
if str_eq(rem_parent_email, "") || str_eq(rem_child_email, "") {
return "{\"error\":\"parent_email and child_email required\"}"
}
let rem_sb_url: String = state_get("__supabase_project_url__")
let rem_sb_key: String = state_get("__supabase_service_key__")
if str_eq(rem_sb_key, "") {
return "{\"error\":\"not configured\"}"
}
// Fetch the subscription ID
let sub_lookup: String = http_get_auth(
rem_sb_url + "/rest/v1/family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email + "&select=stripe_subscription_id",
rem_sb_key
)
let rem_sub_id: String = json_get(sub_lookup, "0.stripe_subscription_id")
// Cancel Stripe subscription if present
if !str_eq(rem_sub_id, "") {
let rem_stripe_key: String = state_get("__stripe_secret_key__")
if !str_eq(rem_stripe_key, "") {
let cancel_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions/" + rem_sub_id + "/cancel", rem_stripe_key, "")
println("[family/remove] cancel sub " + rem_sub_id + " -> " + cancel_resp)
}
}
// Update status to cancelled
let rpe_safe: String = str_replace(str_replace(rem_parent_email, "\\", "\\\\"), "\"", "\\\"")
let rce_safe: String = str_replace(str_replace(rem_child_email, "\\", "\\\\"), "\"", "\\\"")
let cancel_row: String = "{\"status\":\"cancelled\"}"
supabase_insert(rem_sb_url, rem_sb_key, "family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email, cancel_row)
println("[family/remove] cancelled " + rce_safe + " from " + rpe_safe)
return "{\"ok\":true}"
}
// API key provisioning POST /api/api-keys
// Returns user's stored provider keys (masked) for display on /account.
// Body: { access_token: "<jwt>" }
@@ -2391,7 +2187,6 @@ let stripe_key: String = env("STRIPE_SECRET_KEY")
let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY")
let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING")
let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL")
let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD")
let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET")
let license_api_url: String = env("NEURON_LICENSE_API_URL")
let resend_api_key: String = env("RESEND_API_KEY")
@@ -2442,7 +2237,6 @@ state_set("__stripe_secret_key__", stripe_key)
state_set("__stripe_publishable_key__", stripe_pub_key)
state_set("__stripe_price_founding__", stripe_price_founding)
state_set("__stripe_price_professional__", stripe_price_professional)
state_set("__stripe_price_family_child__", family_child_price)
state_set("__license_api_url__", license_api_url)
state_set("__resend_api_key__", resend_api_key)
state_set("__supabase_anon_key__", supabase_anon_key)
+1 -1
View File
@@ -77,7 +77,7 @@ fn marketplace_categories() -> String {
el_div(
"class=\"marketplace-categories reveal\" style=\"transition-delay:320ms\"",
marketplace_tags_block("Connectors - day one", connectors) +
marketplace_tags_block("Connectors (rolling out)", connectors) +
marketplace_tags_block("Following launch", following) +
el_div(
"",
+8 -8
View File
@@ -1,4 +1,4 @@
// components/pricing.el - Three pricing cards: Free / Professional / Founding Member.
// components/pricing.el - Three pricing cards: Free trial / Professional / Founding Member.
// Accepts the founding counter values as parameters so main.el can inject
// server-side rendered counts without JS.
@@ -36,7 +36,7 @@ fn pricing_free_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Local inference via Ollama (coming)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference included when it launches - Q3 2026")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Unlimited projects")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "3 marketplace plugins included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Plugin marketplace access (coming soon)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Core built-in capabilities"))
}
@@ -51,9 +51,9 @@ fn pricing_pro_features() -> String {
}
fn pricing_founding_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - pay-per-use at the founding member rate, below the major APIs")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "No subscription — software updates are free forever")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
@@ -89,14 +89,14 @@ fn pricing(sold: Int, total: Int) -> String {
el_div(
"class=\"pricing-price-row\"",
el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "forever")
el_span("class=\"pricing-cadence\"", "for 14 days")
) +
el_p("class=\"pricing-tagline\"", "Start building your memory. Card required for age verification — you won't be charged.") +
el_p("class=\"pricing-tagline\"", "Try everything free for 14 days. No card required.") +
el_ul("class=\"pricing-features\"", pricing_free_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
"class=\"pricing-cta pricing-cta-ghost\"",
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Preorder free tier &#8594;")
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Start free trial &#8594;")
)
)
@@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$199") +
el_span("class=\"pricing-cadence\"", "lifetime")
) +
el_p("class=\"pricing-tagline\"", "Pay once for the platform — free software updates, forever. Inference is pay-per-use at your founding member rate.") +
el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") +
spots_html +
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
el_div("style=\"flex:1\"", "") +
+2 -2
View File
@@ -78,9 +78,9 @@ fn page_head() -> String {
return page_head_base()
+ page_seo_block(
"Neuron — The AI That Remembers You",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start free.",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start with a 14-day free trial — no credit card required.",
"/",
"Every other AI forgets you. Neuron doesn&#39;t. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. Free tier available."
"Every other AI forgets you. Neuron doesn&#39;t. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. 14-day free trial."
)
+ page_schema()
}