Compare commits

..

2 Commits

Author SHA1 Message Date
will.anderson 0a4d454765 ci: re-trigger #3 2026-05-11 18:25:41 -05:00
will.anderson c4c30f1b33 ci: re-trigger after runner restart 2026-05-11 17:59:40 -05:00
11 changed files with 63 additions and 177 deletions
+1 -6
View File
@@ -82,12 +82,7 @@ jobs:
echo "Changed files:" echo "Changed files:"
echo "$CHANGED" 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) 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 if [ -z "$NON_ASSET" ] && [ "$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
echo "asset_only=true" >> "$GITHUB_OUTPUT" echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path" echo "=> Asset-only change detected, will use fast path"
else else
-1
View File
@@ -9,7 +9,6 @@
# #
# CI pre-build steps (in stage.yaml): # CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing # - neuron-web: built by `elb build` → dist/neuron-landing
# Last rebuilt: 2026-05-11
FROM ubuntu:24.04 FROM ubuntu:24.04
-1
View File
@@ -1,4 +1,3 @@
// elhtml_impl.c — El HTML element stubs.
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include "el_runtime.h" #include "el_runtime.h"
+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. */ * Allows serving PNGs and other binary files without strlen truncation. */
static _Thread_local size_t _tl_fs_read_len = 0; 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) { static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return; if (!_tl_arena_active || !p) return;
if (_tl_arena.count >= _tl_arena.cap) { 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; 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 /* 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 * with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for
* normal text/JSON responses where _tl_fs_read_len is 0. */ * normal text/JSON responses where _tl_fs_read_len is 0. */
size_t blen = binary_side_channel ? _tl_binary_size size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */ _tl_fs_read_len = 0; /* consume — one-shot per response */
int head_only = _tl_http_head_only; 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); if (env_parsed_root) el_release(env_parsed_root);
free(env_body); free(env_body);
free(hdrs.buf); 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 { 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); const char* b = EL_CSTR(body);
if (!b) b = ""; 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 /* 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, * 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) * 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. */ * http_send_response and http_parse_envelope. */
_tl_fs_read_len = 0; _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); JsonBuf out; jb_init(&out);
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */ jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
jb_puts(&out, ",\"status\":"); 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, ",\"headers\":");
jb_puts(&out, hj); jb_puts(&out, hj);
jb_puts(&out, ",\"body\":"); jb_puts(&out, ",\"body\":");
if (binary_len > 0 && _tl_binary_body) { jb_emit_escaped(&out, b);
/* Sentinel: http_send_response() will substitute the real bytes. */
jb_puts(&out, "\"__el_binary__\"");
} else {
jb_emit_escaped(&out, b);
}
jb_putc(&out, '}'); jb_putc(&out, '}');
return el_wrap_str(out.buf); return el_wrap_str(out.buf);
} }
+1 -25
View File
@@ -345,31 +345,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
} }
fn checkout_style_html() -> String { fn checkout_style_html() -> String {
let css: String = ".checkout-shell { let css: String = ".checkout-plan-name {
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 {
font-family: var(--head); font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem); font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600; font-weight: 600;
+20 -32
View File
@@ -142,27 +142,14 @@ fn main() -> Void {
} }
} }
function _todayUTC() { return Math.floor(Date.now() / 86400000); }
function loadSession() { function loadSession() {
try { try {
var s = localStorage.getItem('neuron_demo_session'); var s = localStorage.getItem('neuron_demo_session');
var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' }; return 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;
} catch(e) { return { messages: [], count: 0, context: '' }; } } catch(e) { return { messages: [], count: 0, context: '' }; }
} }
function saveSession(session) { function saveSession(session) {
try { try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
if (!session.day) session.day = _todayUTC();
localStorage.setItem('neuron_demo_session', JSON.stringify(session));
} catch(e) {}
} }
function clearSession() { function clearSession() {
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {} try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
@@ -538,33 +525,34 @@ fn main() -> Void {
// Server-side rate limit — show a live countdown to reset // Server-side rate limit — show a live countdown to reset
if (d.rate_limited && d.reset_at) { if (d.rate_limited && d.reset_at) {
var _showRateTimer = function() { 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 secsLeft = Math.max(0, d.reset_at - now);
var hh = Math.floor(secsLeft / 3600); var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60); var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60; var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; 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 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()); 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 _timerInterval = setInterval(function() {
var msgsEl = document.getElementById('neuron-demo-messages'); var thMsgsInner = document.getElementById('neuron-demo-msgs');
if (!msgsEl) { clearInterval(_timerInterval); return; } if (!thMsgsInner) { clearInterval(_timerInterval); return; }
var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai'); var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
var lastAi = aiMsgs[aiMsgs.length - 1]; var lastAi = aiMsgs[aiMsgs.length - 1];
var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null; if (lastAi) { lastAi.textContent = _showRateTimer(); }
if (lastBubble) { lastBubble.textContent = _showRateTimer(); }
if (Math.floor(Date.now() / 1000) >= d.reset_at) { if (Math.floor(Date.now() / 1000) >= d.reset_at) {
clearInterval(_timerInterval); clearInterval(_timerInterval);
if (lastBubble) { lastBubble.textContent = \"You're all set — conversations reset. Say hello!\"; } if (lastAi) { lastAi.textContent = \"You're all set — conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; } if (btn) { btn.disabled = false; }
msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown();
} }
}, 1000); }, 1000);
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; } if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
if (btn) { btn.disabled = true; } if (btn) { btn.disabled = true; }
if (btn) { btn.disabled = false; }
if (input) { input.focus(); }
return; return;
} }
+17 -3
View File
@@ -1,6 +1,20 @@
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe // checkout-free.el -- Free plan: show success panel after auth completes.
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age // Watches the auth-badge element; when it becomes visible, hides the auth
// verification. This file is no longer compiled or loaded. // section and shows the free-success panel. No card required for free tier.
// Compiled with: elc --target=js --bundle --minify --obfuscate
fn main() -> Void { 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'; if (spinner) spinner.style.display = loading ? '' : 'none';
} }
// Free plan: Stripe SetupIntent for age verification (card saved, never charged). // Free plan has no payment form — bail out entirely.
// Falls through to the same Stripe init path — server returns setup_mode=true for free. if (str_eq(PLAN, 'free')) return;
window._neuronMode = 'payment'; window._neuronMode = 'payment';
var paymentEl = null; var paymentEl = null;
@@ -101,7 +101,7 @@ fn main() -> Void {
if (submitLabel) { if (submitLabel) {
submitLabel.textContent = window._neuronMode === 'setup' submitLabel.textContent = window._neuronMode === 'setup'
? 'Save my card - no charge today →' ? 'Save my card - no charge today →'
: PLAN === 'free' ? 'Verify age & get started →' : 'Complete purchase →'; : 'Complete purchase →';
} }
waitForStripe(function() { waitForStripe(function() {
if (!stripe) stripe = Stripe(STRIPE_PK); if (!stripe) stripe = Stripe(STRIPE_PK);
+14 -38
View File
@@ -637,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." "Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
} else { } else {
if str_eq(plan, "free") { 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." "Create your free Neuron account. No credit card required. Your AI that remembers you runs on your machine, never resets."
} else { } else {
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys." "Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
} }
@@ -697,21 +697,23 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
} }
} }
// Free tier: $0 PaymentIntent for age verification (18+ requirement). // Free tier: creates a SetupIntent for age verification (18+ requirement).
// Verifies card is valid. No charge, no capture. // No charge but the user must provide a valid payment method.
// Note: setup_future_usage cannot be used with amount=0.
if str_eq(plan, "free") { if str_eq(plan, "free") {
let free_pi_body: String = "amount=0" let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&currency=usd" + "&usage=off_session"
+ "&payment_method_types[]=card"
+ "&metadata[plan]=free" + "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification" + "&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_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
let free_pi_resp: String = http_post_form_auth( let free_si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/payment_intents", "https://api.stripe.com/v1/setup_intents",
free_pi_body, free_si_body,
auth_header) 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 // Setup-mode path: save payment method, do not charge. Only valid
@@ -873,32 +875,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return "{\"rows\":" + ac_resp + "}" 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 // My plan: server-side waitlist read with JWT verification
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase // POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
// /auth/v1/user, extract the email, then read the waitlist row with the // /auth/v1/user, extract the email, then read the waitlist row with the
+4 -4
View File
@@ -51,9 +51,9 @@ fn pricing_pro_features() -> String {
} }
fn pricing_founding_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("", "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("", "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("", "Private founding member community")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) + el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
@@ -91,7 +91,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$0") + el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "forever") el_span("class=\"pricing-cadence\"", "forever")
) + ) +
el_p("class=\"pricing-tagline\"", "Start building your memory. Card required for age verification — you won't be charged.") + el_p("class=\"pricing-tagline\"", "Start building your memory. No card required.") +
el_ul("class=\"pricing-features\"", pricing_free_features()) + el_ul("class=\"pricing-features\"", pricing_free_features()) +
el_div("style=\"flex:1\"", "") + el_div("style=\"flex:1\"", "") +
el_div( el_div(
@@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$199") + el_span("class=\"pricing-price\"", "$199") +
el_span("class=\"pricing-cadence\"", "lifetime") 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 + spots_html +
el_ul("class=\"pricing-features\"", pricing_founding_features()) + el_ul("class=\"pricing-features\"", pricing_founding_features()) +
el_div("style=\"flex:1\"", "") + el_div("style=\"flex:1\"", "") +
+1 -1
View File
@@ -78,7 +78,7 @@ fn page_head() -> String {
return page_head_base() return page_head_base()
+ page_seo_block( + page_seo_block(
"Neuron — The AI That Remembers You", "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 free — 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. Free tier available."
) )