fix(opencode,server): string-aware JSONC stripping via shared util/jsonc (#806)
opencode.jsonc/kilo.jsonc with URLs in string values (e.g. "$schema":
"https://opencode.ai/config.json") were corrupted by the adapter's naive
stripJsonComments regex (/\/\/.*$/gm eats everything after "https:"),
so readSettings() returned null and ctx doctor emitted the false
"[FAIL] Plugin configuration: Could not read opencode.json or
opencode.jsonc".
- add src/util/jsonc.ts: two-pass string-aware comment + trailing-comma
stripping and tolerant parseJsonc (byte-identical to the #787-reviewed
implementation so the branches merge cleanly)
- opencode adapter: drop the naive regex stripper (introduced in 7189ed6
to avoid a jsonc-parser dependency; the shared util keeps that
property — zero imports, esbuild inlines it), route through the util
- server.ts: drop its local copy whose trailing-comma regex corrupted
commas inside string values; import the shared util
- tests: URL-bearing opencode.jsonc/kilo.jsonc through the public
adapter API, in-string comma regression, escaped quotes, CRLF, block
comments with URLs, trailing commas, plain-JSON passthrough
Verified end-to-end on real OpenCode 1.17.1: URL-bearing opencode.jsonc
parses, doctor reports PASS, plugin loads and captures session events.
Closes#806
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(server): canonicalize cacheRoot in healCacheMidSession via realpathSync for symlinked ~/.claude (#795)
When ~/.claude is a symlink to another volume, healCacheMidSession
computed cacheRoot via path.resolve (no symlink dereference) but the
plugin registry stores installPath as a physical path. The
startsWith guard rejected every entry, skipping the mid-session
cache heal entirely.
Fix: canonicalize cacheRoot with realpathSync (try/catch fallback)
before the traversal guard, matching the pattern already used in
cli.ts upgrade() and statuslineForward().
* fix(test): use realpathSync on installPath in symlink containment test
On macOS /var is a symlink to /private/var. fs.realpathSync(lexicalCacheRoot)
resolves ALL symlinks including the filesystem-level /var→/private/var, but
resolve(physicalInstallPath) stays on the non-canonical side, causing the
startsWith check to fail.
Apply realpathSync symmetrically to both sides of the comparison so the
canonical paths match regardless of platform-level symlinks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(runtime): add liveness guard to resolveJavascriptRuntime for Homebrew Cellar ENOENT (#800)
When process.execPath is a Homebrew Cellar path
(/opt/homebrew/Cellar/node/VERSION/bin/node), brew upgrade
plus brew cleanup deletes the old Cellar directory, leaving the
in-process execPath dangling. The liveness guard falls back to
PATH-resolved node when the execPath no longer exists on disk.
Preserves the snap-node fix (#190): the snap wrapper binary
literally exists at its execPath, so the guard passes and the
path is returned verbatim (not collapsed to bare node).
- Add existsSync(execPath) check before returning execPath
- Existing snap-node + bun regression tests updated to mock
existsSync returning true for their respective exec paths
- Two new tests covering:
1. Stale Cellar path falls back to PATH node
2. Stale Cellar + no PATH node returns null
* fix(test): cross-platform bun path mock in #800 Cellar ENOENT test
The existsSync mock only blocked Unix-style bun paths
(/.bun/bin/bun), but on Windows bunFallbackPaths returns
paths with backslashes (C:\Users\...\.bun\bin\bun.exe).
This caused bunExists() to find a real bun on the Windows CI
runner, making resolveJavascriptRuntime return the bun path
instead of 'node'.
Replace the single-platform .includes() check with a
cross-platform regex that matches both file separators
and the optional dot prefix on ~/.bun.
Per Mert directive 2026-06-07. Enterprise tier surface is being
pulled until the product story is clearer. Strips:
- src/routes/enterprise.tsx (full page: persona grid EM/CISO/DevOps,
2x2 value props, design-partner mailto CTA)
- Enterprise nav entry + Building2 icon import from src/routes/__root.tsx
- Auto-regenerated src/routeTree.gen.ts without /enterprise route
Per Mert directive 2026-06-07:
- Brand normalized to "Context Mode Insight" (singular, capital I).
"Insights" plural → "Insight". 34 substitutions.
- "Analytics" / "analytics" eliminated everywhere — 10 user-facing
occurrences across index.html + oss.html, all → "Insight" /
"insight".
- twitter:creator @AcikAnaliz → @mksglu.
URL surface stays minimal: only / and /oss. Dropped the legacy
/insights alias from worker.js — no /insight or /insights route
exists. Canonical and og:url back to https://context-mode.com/.
Added baseline SEO files:
- robots.txt (Allow all + sitemap pointer)
- sitemap.xml (two URLs — / and /oss)
- og:image meta points to /og-insight.png (asset still TODO,
declared so the OG harvester knows what to expect)
Title trimmed for SERP fit:
"Context Mode Insight — AI engineering signal at $20/seat" (56 ch)
Owner directive (2026-06-07): "layout u bozuyor biraz ortali oldugu icin,
pricing'de yaptigin gibi yap" — the FAQ chapter was the only chapter using
wrap-narrow (840px tight-center) while every other chapter uses wrap
(1280px) with an inline-centered intro block. The inconsistency broke
page rhythm.
Mirror the Pricing chapter pattern shipped in 48cfe06 (web/index.html
L461-468):
- section uses regular .wrap container (1280px) instead of .wrap-narrow
- intro block .rev: max-width:880px; margin:0 auto; text-align:center
- .rule: margin-left:auto; margin-right:auto
- .faq details list: max-width:840px; margin auto (preserves the
comfortable reading width of wrap-narrow inside a centered column
while the questions/answers stay left-aligned by default)
Also adds id="faq" to the chapter for anchor parity with id="pricing".
Files:
- web/index.html L1292-1301 (FAQ intro block — was using wrap-narrow
+ no centering styles → now matches pricing's centered-axis pattern)
- mirrors web/index.html L461-468 (pricing intro block — the
canonical centered-axis treatment)
No content change. Visual treatment only — every <details> question +
answer body is byte-identical.
Mert directive: "What this is not" deny-list section ("No SSO. No SOC 2
report. No on-prem option ...") removed entirely. The pricing chapter
also dropped the competitor-peer-tier callout ("Same band as Cursor
Pro, GitHub Copilot Business, Vercel Pro — the AI-dev-tool tier you
already budget for") which Mert flagged as unnecessary mention.
The remaining pricing chapter elements are now centered (kicker, H2,
rule, lead, card) so the chapter reads as one focused vertical block
instead of the prior left-headline + center-card asymmetry that broke
the page rhythm.
Also softened the SSO/SOC 2 FAQ answer:
before: "If your buying process requires SSO or a SOC 2 report, run
the OSS collector self-hosted (free, ELv2 source-available);
we will revisit Enterprise after the first Series A."
after: "The OSS collector is source-available under ELv2 and can be
run self-hosted if your buying process requires those
controls."
The Series A reference removed (out of band for buyer-facing copy);
competitor mentions in the FAQ removed.
Net delta: 5 line removal in pricing chapter + 1 line FAQ rewrite.
The Linear / Notion / Jira mentions in the §Integrations chapter stay
— those are functional integration partners, not pricing peers.
Wave PS (platform repo, commit d511924) deleted the /signup route
entirely. Card capture moved to Stripe-hosted Checkout via the OAuth
callback → /?signup=needs_checkout&… → POST /auth/signup-with-checkout
flow. Per Mert directive: "Bizim Platform'da /signup isminde bir
endpoint imiz bir Page'imiz yok ... Signup olmamalı."
Every CTA on this landing pointed at platform.context-mode.com/signup
which now returns 404 (route removed in routeTree.gen.ts). This commit
retargets all 11 references to the platform root:
- L39 JSON-LD offers.url
- L441 §01 chapter CTA
- L549 §02 chapter CTA
- L1166 §08 chapter CTA
- L1275 §09 chapter CTA
- L1308 pricing card CTA
- L1327 lead paragraph link
- L1396 FAQ answer link
- L1441 §10 chapter CTA
- L1459 footer micro-copy
- L1479 sticky bottom CTA
The platform root (https://platform.context-mode.com) renders the
WelcomeLanding component for unauthenticated visitors (Google + GitHub
OAuth buttons). After OAuth, the new-user path lands on Home with the
?signup=needs_checkout marker which kicks off Stripe Checkout.
No copy change beyond the URL — every "Start for $20 / seat" button
still says the same thing, just points one URL segment shorter.
Owner directive 2026-06-06: pricing needs prioritized work — make
it more prominent. $20/seat is the value claim; burying it nine
chapters deep was wrong for a self-serve funnel.
Option A (per the directive): relocate the entire <section
id="pricing"> block to immediately after the trust strip, before
the PROBLEM chapter (§01). Visitors now see the price band within
the first scroll after they see the hero + trust signal.
- Cut <section id="pricing"> block (36 lines incl card, deny-list)
from between SECURITY (§10) and FAQ
- Paste it directly after the trust strip's closing </section>,
before the PROBLEM (§01) chapter
- Drop the "§ 11 · pricing" section-num prefix — pricing is no
longer a numbered chapter in the storytelling sequence; it is
a top-funnel anchor right after the trust signal. The remaining
§01-§10 numbered chapters keep their ordering. FAQ's existing
"§ 12 · faq" label is preserved (pre-existing pattern, not
touched by this wave)
- pricing-card, pricing-cta, pricing-fine, pricing-deny-list CSS
classes are untouched and continue to render the card identically
- The #pricing anchor still resolves; in-page links to #pricing
(none currently exist in this file) would still jump correctly
Storytelling rhythm post-move:
Hero -> Trust strip -> Pricing (NEW slot) -> Problem (§01) ->
Solution (§02) -> ... -> Security (§10) -> FAQ -> Footer
Verify: section count unchanged (13 open / 13 close); id="pricing"
appears once; pricing block precedes the §01 problem chapter.
Owner reported (2026-06-06) the trust strip rendered with 20
company names stacked in a tall right-side column instead of a
horizontal wrap-row.
Root cause: .trust-grid used display:grid with grid-template-columns:
auto 1fr. The first column (auto) sized to its content — the
trust-headline block contains a long sentence + <br> + lead sentence,
so 'auto' computed to ~75% of viewport width. The second column
(1fr) got only the leftover narrow strip, forcing 20 brand spans
to wrap one-per-line vertically. The 900px media-query breakpoint
hid the bug on small screens but not desktop.
Fix (surgical, no redesign):
- .trust-grid: display:grid;grid-template-columns:auto 1fr;gap:48px
-> display:flex;flex-direction:column;gap:20px
- .trust-headline: add max-width:880px so headline doesn't span
the full container on ultrawide
- .trust-brands: add align-items:center for vertical rhythm with
the row above (flex-wrap:wrap was already correct)
- Replace 900px breakpoint with a 600px breakpoint that just
tightens brand gap/font — desktop is now correct, mobile too
Result: headline renders on top (max-width 880px), brands wrap
underneath as a horizontal row that flows naturally across 1-5
visual rows depending on viewport width.
Append canonical OSS/Platform split paragraph at the end of the
"Insights - the analytics layer" chapter body, before the gtm-strip
CTA per PRD §4.3:
"The OSS plugin stays free forever - it is the collector. The
Platform is where your team sees what's happening. Pro tier is a
flat \$20 per seat per month. No tiers, no trial, no surprise
metering."
This is the canonical sentence Mert can cite in any future copy
review. New .chapter-close CSS hook (--bg-alt panel, --accent
left border, --accent-dark emphasis) so the line reads as a closing
emphasis without breaking the section's existing rhythm.
Replace the lingering "Join the waitlist" gtm-strip CTA at the end
of this chapter with "Start for \$20 / seat" so the visual primary
path stays consistent with hero / pricing / FAQ.
L1437 before:
"Private beta . onboarding 5 design-partner orgs . free Team-tier
access during the design phase . shape the patterns we ship next."
After:
"Pro \$20 / seat / month . platform.context-mode.com . OSS
collector free forever."
Per PRD §4.11. Final forbidden-string elimination at the footer
level. Adds an underlined link wrapper on platform.context-mode.com
so the line itself converts.
The page is long (1.5K+ lines, multi-screen scroll). Persistent
bottom-of-viewport CTA improves conversion without breaking chapter
rhythm per PRD §4.10.
HTML: fixed-position bar appended before </body> with:
- "Pro \$20 / seat / month. No tiers, no trial, no surprise bills."
- "Start ->" button to platform.context-mode.com/signup
CSS in existing <style> block using available vars (--bg-alt for
panel, --border for top-edge, --text/--text-2 typography,
--accent-dark hover). No --bg-elevated -> --bg-alt instead.
translateY(100%) initial state, .visible toggles to translateY(0)
with 350ms ease transition. 640px breakpoint stacks vertical.
JS: passive scroll listener flips .visible after scrollY exceeds
80vh. aria-hidden flips in sync so screen readers ignore the bar
until it materialises.
Remove 3 stale FAQ pairs (each containing "private beta", "design
partner", "free Team-tier", or "join the waitlist for design-partner
inquiries"):
- "Is Insights live yet? How do I get in?"
- "On-prem option?" (design-partner inquiries)
- "What other Solutions are on the roadmap?" (design partners
welcome) - content folded into "How does Insights relate to
Context Mode Platform?" so Audit/Risk Score is still surfaced
Add 4 new pricing-aligned FAQ pairs at the top per PRD §4.9:
- How much does Insights cost?
- Can I try it free?
- What is your refund policy?
- Do you offer SSO / SAML / SOC 2?
Also fix the "How are the patterns built?" answer which had stale
counts:
- 219 patterns -> 222 patterns (matches catalog)
- 8 categories -> 9 categories (cost category included)
- 10 adapters (Kimi listed) -> 14 adapters (rebuilt list matches
Slice 4 adapter chapter)
- 15 platform integrations on OSS side -> 16 (configs/ directory
count)
FAQ closing gtm-strip CTA: "Still have questions? Join the waitlist"
-> "Start for \$20 / seat" pointing at signup. Hand-onboarding orgs
still reach the waitlist via the demoted Slice 7 form upstream.
Per PRD §4.7 - waitlist stays for orgs that want guided rollout, but
the language drops every "private beta" and "free Team-tier" claim:
- H2: "Insights is in private beta." -> "Want a guided rollout? Tell
us about your team."
- Lead: rewritten to point self-serve customers to
platform.context-mode.com/signup and frame this form as
hand-onboarding only
- monthly_budget radio group removed entirely (price is fixed
\$20/seat x N - no budget band to guess) - replaced with
seat_count number input (min 1, max 500, placeholder "e.g. 47")
- "No card required... 5 design-partner orgs" -> "We will reply
within 24 hours"
- "Early access" bullet -> "Guided rollout" bullet
- "Free Team-tier during design phase" bullet -> "Same Pro tier,
same price" bullet (\$20/seat from day one, cancel via Stripe)
- Section-num "11" -> "12" (pricing took the prior 11 slot)
Also fix trust strip L449 stale "currently in private beta" copy to
match the new self-serve framing - this site is not called out in
PRD audit table but it carried the forbidden "private beta" string.
Form backend unchanged - still POSTs to the same web3forms endpoint.
New section #pricing per PRD §4.8:
- "One tier. Twenty dollars per seat. Predictable." H2
- Pricing card: $20/seat/mo, 8 feature bullets (222 patterns, 14
adapters, 90-day retention, persona views, etc.), CTA to
platform.context-mode.com/signup, fine print on no annual/volume/
trial/free
- Deny list: "What this is not" - no SSO, no SOC 2, no on-prem, no
dedicated support. Revisit Enterprise after Series A.
CSS inline in existing <style> block using established vars:
--border, --border-light, --bg-alt (no --bg-elevated exists), --bg-warm
for deny list, --text/--text-2/--text-3 typography, --mono/--serif
fonts, --accent-dark hover. Mobile breakpoint at 640px shrinks card
padding + price font.
Replace prior "Join the waitlist" gtm-strip CTA on Roles section with
"Start for \$20 / seat" so Roles -> Pricing flows visually toward the
new primary path.
Ground abstract pricing in concrete persona numbers:
- Sarah (CTO) ROI card: "Org pays 47 x \$20 = \$940 / month. Renewal
is a Stripe quantity update, not a procurement event."
- Sophia (FinOps) cost card: "The \$50K/mo AI line item now has a
sibling Platform line: \$940/mo for visibility into the other \$50K."
47-engineer reference org math (47 x \$20 = \$940) becomes the anchor
the rest of the page can cite. New .persona-price-callout CSS using
--accent / --accent-light / --accent-dark vars.
Per PRD §4.4. Bill/Ekrem in PRD map to existing Sarah/Sophia personas.
Add 5 cards (alphabetical within paradigm groups):
json-stdio (8): Claude Code, Codex, Cursor, Gemini CLI,
JetBrains Copilot, Kiro, Qwen Code, VS Code Copilot
ts-plugin (3): Kilo, OpenClaw, OpenCode
mcp-only (3): Antigravity, Pi, Zed
Remove Kimi card (not in canonical 14 per PRD §4.6 list). Sources
verified against src/adapters/ + configs/ inventory. Adapter-paradigm
.mcp CSS class already exists at L303 - no new style needed.
Update hero-sub "10 more AI assistants" -> "9 more" and hero-stat
"10" -> "14" to match. Update chapter lead to "14 AI assistants via
three paradigms". Replace waitlist CTA in gtm-strip with $20 signup
CTA so the chapter's call-to-action matches the new hero primary.
Per PRD §4.6.
- Insert hero-price-anchor under hero-sub: "OSS collects. Platform understands. $20 / seat / month."
- Primary CTA: "Join the waitlist" -> "Start for $20 / seat" -> platform.context-mode.com/signup
- Secondary CTA: "See the personas" -> "Or join the waitlist for hand-onboarding"
- New .hero-price-anchor CSS using existing --accent-light / --accent-dark / --mono vars
Per PRD §4.2. Waitlist demoted to secondary path for hand-onboarding orgs.
Cloudflare Workers Builds auto-deploy did not honour run_worker_first
(likely an older wrangler version on the build runner), so worker.js
never executed and plain asset routing took over. That gave us:
/ -> index.html (OSS content) -- wrong
/oss -> 404
/insights -> insights.html (Insights content) -- wrong place
Swap the filenames so plain asset routing alone produces the correct
URLs:
web/index.html = Insights landing -> served at /
web/oss.html = OSS landing -> served at /oss
worker.js is updated to match the new filenames so the routing stays
consistent whenever run_worker_first does kick in.
The production Worker is named "context-mode" in the Cloudflare
dashboard. wrangler.jsonc said "context-mode-web" which caused
manual `npx wrangler deploy` runs to target a separate Worker
that had no routes bound.
account_id pins the deploy to the correct account so wrangler
does not prompt or pick the wrong one in CI.
Cloudflare Workers Builds appears to override the name from the
dashboard either way (this is how the broken auto-deploy still
landed on the right Worker), but aligning the source removes the
confusion and lets `npx wrangler deploy` from the web/ directory
target the right Worker without flags.
Workers Static Assets parsed web/_redirects and ran its 200 rewrite
rules as 307 redirects, looping / and /oss into /insights forever.
worker.js already routes via env.ASSETS.fetch, so _redirects was
both redundant and broken.
run_worker_first ensures worker.js takes precedence over asset
matching (otherwise / served index.html before reaching the router).
html_handling: "none" disables .html canonicalization that turned
/index.html into a 307 back to /.
Verified after manual wrangler deploy: /, /oss, /insights all return
200 with correct content.
Prevent persona cards from overflowing on mobile
The live Insights landing uses long tool-call and pattern identifiers inside grid cards. Mobile one-column grids still kept min-content widths, so persona content pushed beyond the viewport and duplicated CSS quotes made the first question look broken.
Constraint: / and /insights route to web/insights.html through the Workers router.
Rejected: Copying live HTML into web/index.html | /oss uses index.html, while the broken live route is served from insights.html.
Confidence: high
Scope-risk: narrow
Directive: Keep mobile grid columns as minmax(0, 1fr) when cards contain long code-like tokens.
Tested: Playwright mobile render at 320, 375, 390, and 430 px; document/body width matched viewport and persona cards/tool calls stayed within card bounds. Copilot review follow-up kept persona calls accessible with overflow-x:auto and removed non-standard word-break usage.
Not-tested: Production deploy; this commit only updates the source file.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Owner directive (2026-06-07): "layout u bozuyor biraz ortali oldugu icin,
pricing'de yaptigin gibi yap" — the FAQ chapter was the only chapter using
wrap-narrow (840px tight-center) while every other chapter uses wrap
(1280px) with an inline-centered intro block. The inconsistency broke
page rhythm.
Mirror the Pricing chapter pattern shipped in 48cfe06 (web/index.html
L461-468):
- section uses regular .wrap container (1280px) instead of .wrap-narrow
- intro block .rev: max-width:880px; margin:0 auto; text-align:center
- .rule: margin-left:auto; margin-right:auto
- .faq details list: max-width:840px; margin auto (preserves the
comfortable reading width of wrap-narrow inside a centered column
while the questions/answers stay left-aligned by default)
Also adds id="faq" to the chapter for anchor parity with id="pricing".
Files:
- web/index.html L1292-1301 (FAQ intro block — was using wrap-narrow
+ no centering styles → now matches pricing's centered-axis pattern)
- mirrors web/index.html L461-468 (pricing intro block — the
canonical centered-axis treatment)
No content change. Visual treatment only — every <details> question +
answer body is byte-identical.
Mert directive: "What this is not" deny-list section ("No SSO. No SOC 2
report. No on-prem option ...") removed entirely. The pricing chapter
also dropped the competitor-peer-tier callout ("Same band as Cursor
Pro, GitHub Copilot Business, Vercel Pro — the AI-dev-tool tier you
already budget for") which Mert flagged as unnecessary mention.
The remaining pricing chapter elements are now centered (kicker, H2,
rule, lead, card) so the chapter reads as one focused vertical block
instead of the prior left-headline + center-card asymmetry that broke
the page rhythm.
Also softened the SSO/SOC 2 FAQ answer:
before: "If your buying process requires SSO or a SOC 2 report, run
the OSS collector self-hosted (free, ELv2 source-available);
we will revisit Enterprise after the first Series A."
after: "The OSS collector is source-available under ELv2 and can be
run self-hosted if your buying process requires those
controls."
The Series A reference removed (out of band for buyer-facing copy);
competitor mentions in the FAQ removed.
Net delta: 5 line removal in pricing chapter + 1 line FAQ rewrite.
The Linear / Notion / Jira mentions in the §Integrations chapter stay
— those are functional integration partners, not pricing peers.
Wave PS (platform repo, commit d511924) deleted the /signup route
entirely. Card capture moved to Stripe-hosted Checkout via the OAuth
callback → /?signup=needs_checkout&… → POST /auth/signup-with-checkout
flow. Per Mert directive: "Bizim Platform'da /signup isminde bir
endpoint imiz bir Page'imiz yok ... Signup olmamalı."
Every CTA on this landing pointed at platform.context-mode.com/signup
which now returns 404 (route removed in routeTree.gen.ts). This commit
retargets all 11 references to the platform root:
- L39 JSON-LD offers.url
- L441 §01 chapter CTA
- L549 §02 chapter CTA
- L1166 §08 chapter CTA
- L1275 §09 chapter CTA
- L1308 pricing card CTA
- L1327 lead paragraph link
- L1396 FAQ answer link
- L1441 §10 chapter CTA
- L1459 footer micro-copy
- L1479 sticky bottom CTA
The platform root (https://platform.context-mode.com) renders the
WelcomeLanding component for unauthenticated visitors (Google + GitHub
OAuth buttons). After OAuth, the new-user path lands on Home with the
?signup=needs_checkout marker which kicks off Stripe Checkout.
No copy change beyond the URL — every "Start for $20 / seat" button
still says the same thing, just points one URL segment shorter.