1 Commits

Author SHA1 Message Date
df0bcae250 Merge pull request 'dev' (#1) from dev into main
Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled
Reviewed-on: #1
2026-08-13 11:23:25 +00:00
67 changed files with 429 additions and 5360 deletions

View File

@@ -4,7 +4,7 @@ Two hooks, both small, both readable in a minute, both safe to delete. They exis
| Hook | Event | What it does | | Hook | Event | What it does |
| --- | --- | --- | | --- | --- | --- |
| `session-context.mjs` | `sessionStart` | Injects the session's starting facts: whether the model lanes are bound (and whether the picker drifted from the recorded lead), the handoff's next action, any of the four capped context files currently over its cap, and the standing workflow (commit after each task; `graphify update .` at session end). | | `session-context.mjs` | `sessionStart` | Injects the session's starting facts: whether the model lanes are bound (and whether the picker drifted from the recorded lead), the handoff's next action, and any of the four capped context files currently over its cap. |
| `guard-destructive.mjs` | `beforeShellExecution` | Returns `ask` — never `deny` — for force pushes, `rm -rf`, migrations, deploys, infra changes, pipe-to-shell, and friends, with the reason named and the gate quoted back to the agent. Routine `git push` is deliberately not on the list. | | `guard-destructive.mjs` | `beforeShellExecution` | Returns `ask` — never `deny` — for force pushes, `rm -rf`, migrations, deploys, infra changes, pipe-to-shell, and friends, with the reason named and the gate quoted back to the agent. Routine `git push` is deliberately not on the list. |
## Why these two ## Why these two

View File

@@ -132,10 +132,6 @@ function main(payload) {
notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`); notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`);
} }
notes.push(
"Workflow: commit after each landed task (no secrets). Session end: graphify update .",
);
return notes.length return notes.length
? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` } ? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
: {}; : {};

View File

@@ -1,22 +0,0 @@
---
description: graphify knowledge graph context
alwaysApply: true
---
This project has a graphify knowledge graph at graphify-out/.
**MANDATORY: Before using Read, Grep, Glob, or Bash to explore the codebase, you MUST run graphify first:**
- `graphify query "<question>"` — scoped subgraph for any codebase or architecture question
- `graphify path "<A>" "<B>"` — dependency path between two symbols
- `graphify explain "<concept>"` — all nodes related to a concept
This applies to YOU and to every subagent you spawn. Include this rule explicitly in every subagent prompt that involves code exploration. Do not skip graphify because files are "already known" or because you are executing a plan — the graph surfaces cross-file dependencies and INFERRED edges that grep and Read cannot find.
Only use Read/Grep/Glob directly when:
1. graphify has already oriented you and you need to modify or debug specific lines
2. `graphify-out/graph.json` does not exist yet
- If `graphify-out/wiki/index.md` exists, navigate it instead of reading raw files
- Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review when query/path/explain do not surface enough context
- After modifying code files, run `graphify update .` to keep the graph current (AST-only, no API cost).
- At **session end**, always run `graphify update .` even if the last edits were docs-only, so the next session's queries match the tree.

View File

@@ -1,80 +0,0 @@
name: CI — Chrome
on:
push:
branches: [main, develop]
paths:
- 'packages/chrome/**'
- 'src/lib/**'
- 'tests/**'
- '.gitea/workflows/ci-chrome.yml'
pull_request:
branches: [main]
paths:
- 'packages/chrome/**'
- 'src/lib/**'
- 'tests/**'
- '.gitea/workflows/ci-chrome.yml'
jobs:
chrome:
name: Chrome — typecheck, test, build
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: node:22-bookworm
steps:
- name: Install system deps
run: apt-get update -qq && apt-get install -y git ca-certificates -qq
- name: Clone repository
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /tmp/lexai
cd /tmp/lexai
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
else
git checkout ${{ gitea.sha }}
fi
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai/packages/chrome
- name: Prepare WXT types
run: npx wxt prepare
working-directory: /tmp/lexai/packages/chrome
- name: Typecheck
run: npm run typecheck
working-directory: /tmp/lexai/packages/chrome
- name: Unit tests
run: npm test -- --run
working-directory: /tmp/lexai/packages/chrome
- name: Build
run: npm run build
working-directory: /tmp/lexai/packages/chrome
- name: Verify output
run: ls -la .output/chrome-mv3/
working-directory: /tmp/lexai/packages/chrome
- name: Notify Success
if: success()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=✅ LexAI CI Chrome Passed%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}"
- name: Notify Failure
if: failure()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ LexAI CI Chrome FAILED%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}%0Ahttps://git.juankibin.space/kibin/LexAI/actions"

View File

@@ -1,72 +0,0 @@
name: CI — CLI
on:
push:
branches: [main, develop]
paths:
- 'packages/cli/**'
- 'src/lib/**'
- '.gitea/workflows/ci-cli.yml'
pull_request:
branches: [main]
paths:
- 'packages/cli/**'
- 'src/lib/**'
- '.gitea/workflows/ci-cli.yml'
jobs:
cli:
name: CLI — typecheck, build, smoke
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: node:22-bookworm
steps:
- name: Install system deps
run: apt-get update -qq && apt-get install -y git ca-certificates -qq
- name: Clone repository
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /tmp/lexai
cd /tmp/lexai
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
else
git checkout ${{ gitea.sha }}
fi
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai/packages/cli
- name: Typecheck
run: npm run typecheck
working-directory: /tmp/lexai/packages/cli
- name: Build
run: npm run build
working-directory: /tmp/lexai/packages/cli
- name: Smoke (help + list, no API key)
run: |
node out/cli.js --help
node out/cli.js prompt --list
working-directory: /tmp/lexai/packages/cli
- name: Notify Success
if: success()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=✅ LexAI CI CLI Passed%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}"
- name: Notify Failure
if: failure()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ LexAI CI CLI FAILED%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}%0Ahttps://git.juankibin.space/kibin/LexAI/actions"

View File

@@ -1,70 +0,0 @@
name: CI — VS Code
on:
push:
branches: [main, develop]
paths:
- 'packages/vscode/**'
- 'src/lib/**'
- '.gitea/workflows/ci-vscode.yml'
pull_request:
branches: [main]
paths:
- 'packages/vscode/**'
- 'src/lib/**'
- '.gitea/workflows/ci-vscode.yml'
jobs:
vscode:
name: VS Code — typecheck, build
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: node:22-bookworm
steps:
- name: Install system deps
run: apt-get update -qq && apt-get install -y git ca-certificates -qq
- name: Clone repository
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /tmp/lexai
cd /tmp/lexai
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
else
git checkout ${{ gitea.sha }}
fi
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai/packages/vscode
- name: Typecheck
run: npm run typecheck
working-directory: /tmp/lexai/packages/vscode
- name: Build
run: npm run build
working-directory: /tmp/lexai/packages/vscode
- name: Verify output
run: ls -la out/extension.js
working-directory: /tmp/lexai/packages/vscode
- name: Notify Success
if: success()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=✅ LexAI CI VS Code Passed%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}"
- name: Notify Failure
if: failure()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ LexAI CI VS Code FAILED%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}%0Ahttps://git.juankibin.space/kibin/LexAI/actions"

View File

@@ -1,52 +0,0 @@
name: CI — Website
on:
push:
branches: [main, develop]
paths:
- 'packages/website/**'
- '.gitea/workflows/ci-website.yml'
pull_request:
branches: [main]
paths:
- 'packages/website/**'
- '.gitea/workflows/ci-website.yml'
jobs:
website:
name: Website — typecheck, build
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: node:22-bookworm
steps:
- name: Install system deps
run: apt-get update -qq && apt-get install -y git ca-certificates -qq
- name: Clone repository
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /tmp/lexai
cd /tmp/lexai
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
else
git checkout ${{ gitea.sha }}
fi
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai/packages/website
- name: Typecheck
run: npm run typecheck
working-directory: /tmp/lexai/packages/website
- name: Build
run: npm run build
working-directory: /tmp/lexai/packages/website
- name: Verify output
run: ls -la dist/
working-directory: /tmp/lexai/packages/website

100
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,100 @@
name: CI — Test & Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test-and-build:
name: Test & Build
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: node:22-bookworm
steps:
- name: Install system deps
run: apt-get update -qq && apt-get install -y zip curl git ca-certificates -qq
- name: Clone repository (triggering ref)
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git /tmp/lexai
cd /tmp/lexai
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
else
git checkout ${{ gitea.sha }}
fi
- name: Install Chrome package dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai/packages/chrome
# Generates .wxt/types (ImportMeta.env etc.) required by typecheck.
- name: Prepare WXT types
run: npx wxt prepare
working-directory: /tmp/lexai/packages/chrome
- name: Type check
run: npm run typecheck
working-directory: /tmp/lexai/packages/chrome
- name: Run unit tests
run: npm test -- --run
working-directory: /tmp/lexai/packages/chrome
- name: Build extension
run: npm run build
working-directory: /tmp/lexai/packages/chrome
- name: Verify build output
run: ls -la .output/chrome-mv3/
working-directory: /tmp/lexai/packages/chrome
- name: Package as ZIP
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
TS=$(date +%Y%m%d%H%M%S)
VERSION=$(node -p "require('./package.json').version")-${SHA}-${TS}
zip -r /tmp/lexai-chrome-mv3-${VERSION}.zip .output/chrome-mv3/
echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
working-directory: /tmp/lexai/packages/chrome
- name: Publish to Gitea Package Registry
if: gitea.event_name == 'push'
run: |
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
"${{ gitea.server_url }}/api/packages/kibin/generic/lexai-extension/${PACKAGE_VERSION}/lexai-chrome-mv3-${PACKAGE_VERSION}.zip" \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
-T "/tmp/lexai-chrome-mv3-${PACKAGE_VERSION}.zip")
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP: $HTTP_CODE | Response: $BODY"
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ Published: https://git.juankibin.space/kibin/-/packages"
else
echo "⚠️ Package publish returned $HTTP_CODE: $BODY"
exit 1
fi
env:
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
- name: Notify Success
if: success()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=✅ LexAI CI Passed%0A%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}%0A%0AAll steps green — new build published to packages."
- name: Notify Failure
if: failure()
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ LexAI CI FAILED%0A%0ABranch: ${{ gitea.ref_name }}%0ACommit: ${SHA}%0A%0ACheck: https://git.juankibin.space/kibin/LexAI/actions"

View File

@@ -0,0 +1,132 @@
name: Deploy — Chrome Web Store
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
publish:
description: 'Publish after upload? (yes/no)'
required: false
default: 'yes'
jobs:
deploy:
name: Build & Deploy to Chrome Web Store
runs-on: ubuntu-latest
timeout-minutes: 20
container:
image: node:22-bookworm
steps:
- name: Install deps
run: apt-get update -qq && apt-get install -y zip curl
- name: Checkout
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
git checkout ${{ gitea.sha }}
- name: Install dependencies
run: npm ci
working-directory: packages/chrome
# Same gates as CI — a release must never check less than a push.
- name: Prepare WXT types
run: npx wxt prepare
working-directory: packages/chrome
- name: Type check
run: npm run typecheck
working-directory: packages/chrome
- name: Run tests
run: npm test -- --run
working-directory: packages/chrome
- name: Build extension
run: npm run build
working-directory: packages/chrome
- name: Package as ZIP
run: |
VERSION=${{ gitea.ref_name || 'manual' }}
ZIPFILE="$(cd ../.. && pwd)/lexai-chrome-mv3-${VERSION}.zip"
# Verify manifest exists and check version
node -e "console.log('Manifest version:', require('./.output/chrome-mv3/manifest.json').version)"
# Zip from inside the chrome-mv3 dir so manifest.json is at root
cd .output/chrome-mv3 && zip -r "$ZIPFILE" . && cd -
echo "ZIP_FILE=${ZIPFILE}" >> $GITHUB_ENV
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
working-directory: packages/chrome
- name: Get Chrome Web Store OAuth2 Token
run: |
TOKEN_RESPONSE=$(curl -s -X POST "https://oauth2.googleapis.com/token" \
-d "client_id=${{ secrets.CWS_CLIENT_ID }}" \
-d "client_secret=${{ secrets.CWS_CLIENT_SECRET }}" \
-d "refresh_token=${{ secrets.CWS_REFRESH_TOKEN }}" \
-d "grant_type=refresh_token")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{let d;try{d=JSON.parse(s)}catch(e){console.error('Invalid JSON token response:',s);process.exit(1)};if(!d.access_token){console.error('No access_token in response:',s);process.exit(1)};console.log(d.access_token)})")
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Failed to get access token. Response: $TOKEN_RESPONSE"
exit 1
fi
echo "CWS_ACCESS_TOKEN=${ACCESS_TOKEN}" >> $GITHUB_ENV
echo "✅ OAuth2 token obtained"
- name: Upload to Chrome Web Store
run: |
UPLOAD_BODY=$(curl -s -X PUT \
"https://www.googleapis.com/upload/chromewebstore/v1.1/items/${{ secrets.CWS_EXTENSION_ID }}" \
-H "Authorization: Bearer ${{ env.CWS_ACCESS_TOKEN }}" \
-H "x-goog-api-version: 2" \
-T "${{ env.ZIP_FILE }}")
echo "Upload Response: $UPLOAD_BODY"
UPLOAD_STATE=$(echo "$UPLOAD_BODY" | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{let d;try{d=JSON.parse(s)}catch(e){console.error('Invalid JSON upload response:',s);process.exit(1)};console.log(d.uploadState||'')})")
echo "Upload state: $UPLOAD_STATE"
if [ "$UPLOAD_STATE" != "SUCCESS" ]; then
echo "❌ Upload failed — state: $UPLOAD_STATE"
exit 1
fi
echo "✅ Uploaded to Chrome Web Store"
- name: Publish to Chrome Web Store
if: ${{ gitea.event_name == 'push' || github.event.inputs.publish == 'yes' }}
run: |
PUBLISH_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"https://www.googleapis.com/chromewebstore/v1.1/items/${{ secrets.CWS_EXTENSION_ID }}/publish" \
-H "Authorization: Bearer ${{ env.CWS_ACCESS_TOKEN }}" \
-H "x-goog-api-version: 2" \
-H "Content-Length: 0")
HTTP_CODE=$(echo "$PUBLISH_RESPONSE" | tail -n 1)
BODY=$(echo "$PUBLISH_RESPONSE" | sed '$d')
echo "Publish HTTP: $HTTP_CODE"
echo "Publish Response: $BODY"
STATUS=$(echo "$BODY" | grep -o '"status":\["[^"]*"\]' | head -1)
echo "Publish status: $STATUS"
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ Published to Chrome Web Store!"
else
echo "❌ Publish failed: HTTP $HTTP_CODE"
exit 1
fi
- name: Notify Success
if: success()
run: |
VERSION=${{ env.VERSION }}
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=🚀 LexAI ${VERSION} deployed to Chrome Web Store!%0A%0A✅ Built%0A✅ Packaged%0A✅ Uploaded%0A✅ Published%0A%0Ahttps://chromewebstore.google.com/detail/${{ secrets.CWS_EXTENSION_ID }}"
- name: Notify Failure
if: failure()
run: |
VERSION=${{ env.VERSION }}
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ LexAI CWS Deploy FAILED%0A%0AVersion: ${VERSION}%0A%0ACheck: https://git.juankibin.space/kibin/LexAI/actions"

View File

@@ -0,0 +1,53 @@
name: Preview — PR Build Check
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
preview-build:
name: PR Preview Build
runs-on: ubuntu-latest
container:
image: node:22-bookworm
steps:
- name: Install zip curl
run: apt-get update -qq && apt-get install -y zip curl
- name: Checkout PR
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
git fetch origin pull/${{ gitea.event.pull_request.number }}/head:pr
git checkout pr
- name: Install dependencies
run: npm ci
working-directory: packages/chrome
- name: Type check
run: npm run typecheck
working-directory: packages/chrome
- name: Run unit tests
run: npm test -- --run
working-directory: packages/chrome
- name: Build extension
run: npm run build
working-directory: packages/chrome
- name: Package preview ZIP
run: |
PR_NUM=${{ gitea.event.pull_request.number }}
zip -r ../../lexai-pr-${PR_NUM}-preview.zip .output/chrome-mv3/
echo "✅ PR #${PR_NUM} build successful"
working-directory: packages/chrome
- name: Comment on PR
run: |
PR_NUM=${{ gitea.event.pull_request.number }}
curl -s -X POST "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/issues/${PR_NUM}/comments" \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"## ⚡ LexAI Preview Build Ready\n\n✅ Tests passed\n✅ Build successful\n✅ TypeScript clean\n\nCommit: \`${{ gitea.sha }}\`\"}"

View File

@@ -0,0 +1,102 @@
name: Release — Package & Publish
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Build & Package Release
runs-on: ubuntu-latest
container:
image: node:22-bookworm
steps:
- name: Install zip
run: apt-get update -qq && apt-get install -y zip curl
- name: Checkout
run: |
git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
git checkout ${{ gitea.sha }}
- name: Install dependencies
run: npm ci
working-directory: packages/chrome
- name: Run tests
run: npm test -- --run
working-directory: packages/chrome
- name: Build extension
run: npm run build
working-directory: packages/chrome
- name: Package as ZIP
run: |
VERSION=${{ gitea.ref_name }}
ZIPFILE="$(cd ../.. && pwd)/lexai-chrome-mv3-${VERSION}.zip"
# Zip from inside the chrome-mv3 dir so manifest.json is at root of the archive
cd .output/chrome-mv3 && zip -r "$ZIPFILE" . && cd -
echo "ZIP_FILE=${ZIPFILE}" >> $GITHUB_ENV
working-directory: packages/chrome
- name: Create Release
run: |
VERSION=${{ gitea.ref_name }}
cat > build-release-payload.js <<'EOF'
const fs = require('fs');
const tag = process.env.VERSION;
const version = tag.replace(/^v/, '');
const installSteps = '### Installation\n1. Download ZIP below\n2. Extract it\n3. Open Chrome → chrome://extensions\n4. Enable Developer Mode\n5. Click Load unpacked → select the extracted folder';
let body = '## LexAI ' + tag + '\n\n' + installSteps;
try {
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
const headingRe = new RegExp('^## \\[' + version.replace(/\./g, '\\.') + '\\].*$', 'm');
const match = headingRe.exec(changelog);
if (match) {
const rest = changelog.slice(match.index + match[0].length);
const nextStop = rest.search(/^(## \[|\[[^\]]+\]:\s)/m);
const section = (nextStop === -1 ? rest : rest.slice(0, nextStop)).trim();
body = match[0] + '\n\n' + section + '\n\n' + installSteps;
} else {
console.error('No CHANGELOG.md section found for ' + version + ', falling back to generic body');
}
} catch (e) {
console.error('Could not read CHANGELOG.md, falling back to generic body:', e.message);
}
const payload = {
tag_name: tag,
name: 'LexAI ' + tag,
body,
draft: false,
prerelease: false,
};
fs.writeFileSync('payload.json', JSON.stringify(payload));
EOF
VERSION="$VERSION" node build-release-payload.js
curl -s -X POST "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
-H "Content-Type: application/json" \
-d @payload.json > release.json
cat release.json
RELEASE_ID=$(node -e "const r=require('./release.json'); if(!r.id) { console.error('No release id in response:', JSON.stringify(r)); process.exit(1); } console.log(r.id)")
echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_ENV
- name: Upload ZIP to Release
run: |
RELEASE_ID=${{ env.RELEASE_ID }}
curl -s -X POST "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
-F "attachment=@${{ env.ZIP_FILE }}"
echo "✅ Release ${{ gitea.ref_name }} published!"
- name: Publish to Gitea Package Registry
run: |
VERSION=${{ gitea.ref_name }}
curl -s -X PUT "https://git.juankibin.space/api/packages/kibin/generic/lexai-extension/${VERSION}/lexai-chrome-mv3-${VERSION}.zip" \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
-T "${{ env.ZIP_FILE }}"
echo "✅ Published lexai-chrome-mv3-${VERSION}.zip to package registry"
echo "📦 Download: https://git.juankibin.space/kibin/LexAI/packages"

3
.gitignore vendored
View File

@@ -11,6 +11,3 @@ dist/
# Playwright # Playwright
test-results/ test-results/
.tmp-* .tmp-*
# Graphify knowledge graph (local, regenerable)
graphify-out/

View File

@@ -199,8 +199,7 @@ When the user corrects you, a test or review proves a mistake, or you discover a
## Lessons ## Lessons
- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production. - *(Add one-line prevention rules here as mistakes are verified.)*
- After each landed task, commit (no secrets); session end run `graphify update .`.
## Memory protocol ## Memory protocol
@@ -209,8 +208,8 @@ Subagents have no memory and cannot see this conversation — durable knowledge
## State continuity and proactive improvement ## State continuity and proactive improvement
- Do not rely on the transcript for project state. At the start of a resumed or fresh conversation, invoke `/resume-project` before planning or editing. - Do not rely on the transcript for project state. At the start of a resumed or fresh conversation, invoke `/resume-project` before planning or editing.
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and next smallest action, and promote durable knowledge per the memory protocol. Then **commit** the landed files (no secrets, no `graphify-out/`); do not push unless asked. - Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and next smallest action, and promote durable knowledge per the memory protocol.
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language. Then run `graphify update .` (AST-only) so `graphify-out/` matches the tree. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner. - At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
- After a verified recurring mistake, correction, or workflow gap, invoke `/continuous-improvement` and delegate changes to `system-steward`. - After a verified recurring mistake, correction, or workflow gap, invoke `/continuous-improvement` and delegate changes to `system-steward`.
- Improve agents, skills, and rules only from concrete evidence. Keep changes small and testable; never silently change lane routing, tool permissions, hooks, safety rules, or external-action authority. - Improve agents, skills, and rules only from concrete evidence. Keep changes small and testable; never silently change lane routing, tool permissions, hooks, safety rules, or external-action authority.

View File

@@ -336,8 +336,6 @@ Keep rules general, evidence-backed, and under 20 words. Never add secrets, pers
- Bump `version` in `package.json` only (+ lockfile); the manifest derives it via `pkg.version`. - Bump `version` in `package.json` only (+ lockfile); the manifest derives it via `pkg.version`.
- Style inline; Tailwind classes do nothing until PostCSS is wired. - Style inline; Tailwind classes do nothing until PostCSS is wired.
- Verify CHANGELOG entries against code, not commit subjects or handoff summaries. - Verify CHANGELOG entries against code, not commit subjects or handoff summaries.
- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production.
- After each landed task, commit (no secrets); session end run `graphify update .`.
## Memory protocol ## Memory protocol
@@ -355,8 +353,8 @@ Run `/memory-sync` at a phase change, before ending a long run, or when any capp
## State continuity and proactive improvement ## State continuity and proactive improvement
- Do not rely on the chat transcript for project state. At the start of a resumed, compacted, or fresh session, invoke `/resume-project` before planning or editing. - Do not rely on the chat transcript for project state. At the start of a resumed, compacted, or fresh session, invoke `/resume-project` before planning or editing.
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and the next smallest action, and promote any durable new knowledge per the memory protocol. Then **commit** the landed files (no secrets, no `graphify-out/`); do not push unless asked. - Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and the next smallest action, and promote any durable new knowledge per the memory protocol.
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language: what newly works and how to see it, the *Waiting on you* queue, and what proceeds without them. Then run `graphify update .`. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner. - At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language: what newly works and how to see it, the *Waiting on you* queue, and what proceeds without them. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
- After a verified recurring mistake, user correction, or workflow gap, invoke `/continuous-improvement`. Opus delegates changes to `system-steward`. - After a verified recurring mistake, user correction, or workflow gap, invoke `/continuous-improvement`. Opus delegates changes to `system-steward`.
- Improve agents and skills only from concrete evidence. Keep changes small, testable, and versioned; never silently change model routing, tool permissions, safety rules, or external-action authority. - Improve agents and skills only from concrete evidence. Keep changes small, testable, and versioned; never silently change model routing, tool permissions, safety rules, or external-action authority.

View File

@@ -42,19 +42,6 @@ Shared code lives in `src/lib/`. **Do not** import `@lib/crypto` or `@lib/messag
- **VS Code:** build then F5 or Install from VSIX (`packages/vscode`). - **VS Code:** build then F5 or Install from VSIX (`packages/vscode`).
- **CLI:** set `LEXAI_API_KEY`; never commit keys or put them in `~/.lexai/config.json`. - **CLI:** set `LEXAI_API_KEY`; never commit keys or put them in `~/.lexai/config.json`.
## CI
Gitea Actions runs one workflow per package (path-filtered):
| Workflow | Package |
| --- | --- |
| `.gitea/workflows/ci-chrome.yml` | `packages/chrome` (+ `src/lib`, `tests`) |
| `.gitea/workflows/ci-vscode.yml` | `packages/vscode` (+ `src/lib`) |
| `.gitea/workflows/ci-cli.yml` | `packages/cli` (+ `src/lib`) |
| `.gitea/workflows/ci-website.yml` | `packages/website` |
Deploy / preview / release workflows are not enabled yet.
## Pull request process ## Pull request process
1. Fork / branch from `main` (or `develop` if that is the active integration branch). 1. Fork / branch from `main` (or `develop` if that is the active integration branch).

View File

@@ -5,9 +5,8 @@
| Package | What it is | Docs | | Package | What it is | Docs |
| --- | --- | --- | | --- | --- | --- |
| **[packages/chrome](packages/chrome)** | Chrome MV3 extension — select text on any page → Fix / Rephrase / Shorten / Expand / Explain / Make Prompt | [README](packages/chrome/README.md) · [Chrome Web Store](https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk) | | **[packages/chrome](packages/chrome)** | Chrome MV3 extension — select text on any page → Fix / Rephrase / Shorten / Expand / Explain / Make Prompt | [README](packages/chrome/README.md) · [Chrome Web Store](https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk) |
| **[packages/vscode](packages/vscode)** | VS Code / Cursor extension — writing actions + workspace-aware **Code Assist** | [README](packages/vscode/README.md) · [Marketplace](https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode) | | **[packages/vscode](packages/vscode)** | VS Code / Cursor extension — writing actions + workspace-aware **Code Assist** | [README](packages/vscode/README.md) · [Deploy](packages/vscode/DEPLOY.md) |
| **[packages/cli](packages/cli)** | CLI **Prompt Builder** — improve a rough idea into a paste-ready prompt | [README](packages/cli/README.md) | | **[packages/cli](packages/cli)** | CLI **Prompt Builder** — improve a rough idea into a paste-ready prompt | [README](packages/cli/README.md) |
| **[packages/website](packages/website)** | Marketing site + docs (features & all packages) | [README](packages/website/README.md) |
Shared provider/prompt core: [`src/lib`](src/lib). Shared provider/prompt core: [`src/lib`](src/lib).
@@ -26,17 +25,12 @@ npm run chrome:dev
# VS Code extension # VS Code extension
npm run vscode:build npm run vscode:build
# or install: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode # then F5 / Install from VSIX — see packages/vscode/README.md
# CLI Prompt Builder # CLI Prompt Builder
npm run cli:build npm run cli:build
export LEXAI_API_KEY=sk-... # required; PowerShell: $env:LEXAI_API_KEY="sk-..." export LEXAI_API_KEY=sk-... # PowerShell: $env:LEXAI_API_KEY="sk-..."
export LEXAI_PROVIDER=openai # optional: openai | anthropic | groq | openrouter
export LEXAI_MODEL=gpt-4o # optional
node packages/cli/out/cli.js prompt "add rate limiting to the auth routes" node packages/cli/out/cli.js prompt "add rate limiting to the auth routes"
# Website (features + docs)
npm run website:dev
``` ```
## Repository layout ## Repository layout
@@ -46,8 +40,7 @@ LexAI/
├── packages/ ├── packages/
│ ├── chrome/ # Manifest V3 extension (WXT + React) │ ├── chrome/ # Manifest V3 extension (WXT + React)
│ ├── vscode/ # VS Code / Cursor extension │ ├── vscode/ # VS Code / Cursor extension
│ ├── cli/ # lexai prompt CLI │ └── cli/ # lexai prompt CLI
│ └── website/ # Marketing site + docs
├── src/lib/ # Shared providers, actions, types ├── src/lib/ # Shared providers, actions, types
├── tests/ # Unit + e2e tests (run via chrome package) ├── tests/ # Unit + e2e tests (run via chrome package)
├── docs/ # Project operating docs (agents / planning) ├── docs/ # Project operating docs (agents / planning)
@@ -63,7 +56,7 @@ OpenAI · Anthropic · Groq · OpenRouter
- No LexAI servers and no LexAI telemetry - No LexAI servers and no LexAI telemetry
- Chrome: key encrypted in `chrome.storage.local` - Chrome: key encrypted in `chrome.storage.local`
- VS Code: key in Secret Storage - VS Code: key in Secret Storage
- CLI: key via `LEXAI_API_KEY` only; optional `LEXAI_PROVIDER` / `LEXAI_MODEL` (never put the key in `~/.lexai/config.json`) - CLI: key via `LEXAI_API_KEY` environment variable only
## Contributing ## Contributing
@@ -76,5 +69,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT
## Links ## Links
- Chrome Web Store: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk - Chrome Web Store: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
- VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
- Source: https://git.juankibin.space/kibin/LexAI - Source: https://git.juankibin.space/kibin/LexAI

View File

@@ -22,26 +22,6 @@ Add an entry when a choice is hard to reverse, shapes future work, or a future m
## Log ## Log
### D-2026-09-04-01 — Commit each landed task; graphify update at session end
- **Status:** accepted
- **Context:** Graph and git lagged behind work (SEO sat uncommitted; graph needed a manual deep rebuild).
- **Decision:** After every completed task, commit landed files (never secrets or `graphify-out/`). Do not push unless asked. At session end, run `graphify update .`.
- **Alternatives considered:** Commit only when asked (lost work); full `/graphify --mode deep` every session (too expensive).
- **Consequences:** More small commits on `dev`. Graph stays AST-current; docs/images still need an occasional deep extract.
- **Verification:** Session-start hook reminds the workflow; `graphify-out/` stays gitignored.
- **Owner / date:** John Kevin, 2026-09-04
### D-2026-09-03-01 — Website path URLs for Google indexing
- **Status:** accepted
- **Context:** The marketing site used HashRouter (`/#/docs`). Google does not treat hash fragments as indexable URLs. Production already had SPA fallbacks.
- **Decision:** Switch to BrowserRouter; prerender unique HTML for `/`, `/docs`, `/contribute`, `/community`; ship `/robots.txt` and `/sitemap.xml`; unknown paths return HTTP 404; rewrite legacy `#/…` hashes on load.
- **Alternatives considered:** Keep hashes (rejected: not crawlable as pages); full SSR (rejected: extra stack for a static brochure).
- **Consequences:** Coolify/nginx must keep SPA fallback for the four routes. Old social posts with `#/docs` still work via the hash rewrite.
- **Verification:** `npm run website:build`; GET `/robots.txt`, `/sitemap.xml`, `/docs` 200 with unique titles; GET `/no-such-page` 404.
- **Owner / date:** John Kevin / lead, 2026-09-03
### D-2026-08-13-01 — VS Code port: shared `src/lib` + native v1 UX ### D-2026-08-13-01 — VS Code port: shared `src/lib` + native v1 UX
- **Status:** accepted - **Status:** accepted

View File

@@ -1,36 +0,0 @@
# Design system (LexAI website)
## Principles
- Quiet, professional, minimal. Typography and space do the work.
- Fewest clicks to install or docs.
- Privacy (BYO-key, no LexAI servers) is a first-class message.
## Foundations
| Token | Value |
| --- | --- |
| `--bg` | `#f7f7f5` |
| `--surface` | `#ffffff` |
| `--ink` | `#171717` |
| `--muted` | `#5c5c57` |
| `--line` | `#e6e6e2` |
| `--indigo` | `#4f46e5` (logo tile) |
| `--bolt` | `#f5c14a` (logo bolt) |
| Mark | Flat rounded square + bolt (`LogoMark`) |
| Type | Inter |
Tap targets ≥ 40px; body text contrast AA.
## Components
| Component | Use |
| --- | --- |
| SiteHeader | Wordmark + Docs / Contribute / Community / Star on Gitea |
| Hero | One kicker, one H1, one line, CTAs |
| Feature list | Two-column hairline rows |
| PackageCard | Chrome / VS Code / CLI |
| DocsSection | Package docs blocks |
| Not found | Unknown path: H1 + Home / Docs |
| Breadcrumbs | Inner pages: Home / current |
| FAQ | Home `<details>` list; copy must match FAQ JSON-LD |

View File

@@ -4,15 +4,7 @@ This file contains small, repeatable checks derived from verified failures. Pref
## Active failure-derived checks ## Active failure-derived checks
### E-2026-08-17-01 — Website production server answers GET / _No failure-derived checks yet._
- **Prevents:** L-2026-08-17-01 (`Cannot GET /` on Coolify)
- **Type:** command
- **How to run:** `npm run website:build`; then from `packages/website` `PORT=34567 node server.mjs` and GET `http://127.0.0.1:34567/`
- **Pass condition:** HTTP 200 and body contains `LexAI`; `/healthz` returns `ok`
- **Failure signal:** `Cannot GET /`, connection refused, or 500 `dist/ missing`
- **Cost:** fast
- **Last verified:** 2026-08-17 pass (status 200, title present)
## Eval template ## Eval template

View File

@@ -4,7 +4,7 @@
## Current state ## Current state
- **Outcome:** Standing workflow: commit after each landed task; session end `graphify update .`. - **Outcome:** Monorepo OSS layout — Chrome in `packages/chrome`; VS Code + CLI alongside; root LICENSE/README/CONTRIBUTING/CODE_OF_CONDUCT; Gitea issue/PR templates.
- **Shipped:** `AGENTS.md` / `CLAUDE.md` lessons + state continuity; `.cursor/rules/graphify.mdc`; session-start reminder; `D-2026-09-04-01`. - **Verified:** run `npm run chrome:install` + typecheck/test/build after pull (session in progress).
- **Verified:** docs/rules updated; graphify-out gitignored. - **CI:** workflows use `packages/chrome` for install/build/zip paths.
- **Next smallest action:** Continue product work; this session will commit this change and run `graphify update .`. - **Next smallest action:** `npm run chrome:install && npm run chrome:typecheck && npm run chrome:test && npm run chrome:build` then commit when ready.

View File

@@ -6,25 +6,7 @@ This is the project’s shared, evidence-backed memory of mistakes worth prevent
`AGENTS.md` → `## Lessons` is the canonical active rule list loaded every session. Keep supporting evidence here; mirror an active rule here only when its detail is useful for maintenance. Keep at most 12 active rules, each short and imperative. `AGENTS.md` → `## Lessons` is the canonical active rule list loaded every session. Keep supporting evidence here; mirror an active rule here only when its detail is useful for maintenance. Keep at most 12 active rules, each short and imperative.
### L-2026-09-04-01 — Commit each landed task; session end `graphify update .` _No active guardrails yet._
- **Status:** active
- **Trigger:** Owner asked to stop leaving finished work uncommitted and the knowledge graph stale.
- **Root cause / failure boundary:** Commit-on-request plus no session-end graph refresh.
- **Prevention:** Commit after each landed task (no secrets). Session end: `graphify update .`.
- **Evidence:** Owner instruction 2026-09-04; graphify CLI `update`.
- **Eval:** manual guardrail — git status + graphify-out mtime
- **Owner / review:** lead / if auto-commit becomes noisy
### L-2026-08-17-01 — Serve website `dist/` on `0.0.0.0:$PORT`; do not use Vite preview in production
- **Status:** active
- **Trigger:** Coolify deploy of `packages/website` returned Express/Connect `Cannot GET /`.
- **Root cause / failure boundary:** No `start` script; Nixpacks built `dist/` then started a Node process that did not serve it. README Publish Directory `/dist` is an absolute empty path.
- **Prevention:** `packages/website/server.mjs` + `npm start`, or `packages/website/Dockerfile`. Coolify: Base Directory `packages/website`, static site off.
- **Evidence:** Local `node server.mjs` GET `/` → 200 with LexAI HTML; `/healthz` → 200.
- **Eval:** E-2026-08-17-01
- **Owner / review:** lead / after first successful Coolify deploy
## Recording policy ## Recording policy

View File

@@ -4,11 +4,11 @@
## Verified facts ## Verified facts
- LexAI monorepo: `packages/{chrome,vscode,cli,website}`; shared `src/lib`. - LexAI monorepo: `packages/chrome` (WXT), `packages/vscode`, `packages/cli`; shared `src/lib`.
- Portable core: `providers.ts`, `actions.ts`, `types.ts`. Chrome-only: `crypto.ts`, `messaging.ts`. - Portable core: `providers.ts`, `actions.ts`, `types.ts`. Chrome-only: `crypto.ts`, `messaging.ts`.
- Providers: OpenAI, Anthropic, Groq, OpenRouter — `callProvider` / `PROVIDER_SPECS`. - Providers: OpenAI, Anthropic, Groq, OpenRouter — `callProvider` / `PROVIDER_SPECS`.
- VS Code: Secret Storage; Code Assist; prefs `lexai.*`. - VS Code: Secret Storage; Code Assist; prefs `lexai.*`.
- CLI: `lexai prompt`; `LEXAI_API_KEY` required; optional `LEXAI_PROVIDER` / `LEXAI_MODEL`; `~/.lexai/config.json` never holds apiKey. - CLI: `lexai prompt`; `LEXAI_API_KEY`; optional `~/.lexai/config.json` (no apiKey).
- Chrome entrypoints: `packages/chrome/entrypoints/`; build → `packages/chrome/.output/chrome-mv3/`. - Chrome entrypoints: `packages/chrome/entrypoints/`; build → `packages/chrome/.output/chrome-mv3/`.
- Selection minimum: `MIN_SELECTION_LENGTH` (10). Chrome `onMessage` must `return true`; snapshot before `await`. - Selection minimum: `MIN_SELECTION_LENGTH` (10). Chrome `onMessage` must `return true`; snapshot before `await`.
@@ -17,8 +17,7 @@
- Style Chrome UI with inline styles (Tailwind not wired). - Style Chrome UI with inline styles (Tailwind not wired).
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration. - Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
- VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`. - VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`.
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`, `website:*`. - Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`.
- After each landed task: git commit (no secrets; `graphify-out/` ignored). Session end: `graphify update .`. Do not push unless asked.
## Environment quirks ## Environment quirks
@@ -33,10 +32,8 @@
| Shared lib | `src/lib/` | | Shared lib | `src/lib/` |
| VS Code | `packages/vscode/` | | VS Code | `packages/vscode/` |
| CLI | `packages/cli/` | | CLI | `packages/cli/` |
| Website | `packages/website/` (Vite React; Home, Docs, Contribute, Community). BrowserRouter paths `/docs` etc. Origin `https://lexai.juankibin.space`. Production: `npm start` → `server.mjs`, or `Dockerfile` (nginx :80). Coolify base `packages/website`, static site off. |
| OSS meta | root `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` | | OSS meta | root `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` |
| Issue/PR templates | `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md` | | Issue/PR templates | `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md` |
| CI workflows | `.gitea/workflows/ci-{chrome,vscode,cli,website}.yml` (no deploy/release yet) |
## Expiring notes ## Expiring notes

View File

@@ -2,28 +2,29 @@
> For the owner. What works, how to see it, and what's waiting on you — plain language. > For the owner. What works, how to see it, and what's waiting on you — plain language.
**Updated:** 2026-09-04 · **Overall:** Search Console is live; agents now commit after each task and refresh the code graph at session end **Updated:** 2026-08-13 · **Overall:** Chrome LexAI unchanged; VS Code LexAI v1 ready to try locally
## What works now ## What works now
- Chrome extension: select on any page → AI action → Replace/Copy - Chrome extension (existing): select on any page → AI action → Replace/Copy
- VS Code extension: select in editor → LexAI context menu / Command Palette → selection replaced - VS Code extension (new): select in editor → LexAI context menu / Command Palette → selection replaced
- Website: real paths, sitemap accepted by Google. Repo now has stronger titles, a home FAQ, and richer structured data (not live until you deploy) - Shared LLM core (`src/lib`) used by both; VS Code key lives in Secret Storage
## See it yourself ## See it yourself (VS Code) — about five minutes
1. Merge/push this change, Coolify on **main**, redeploy `packages/website`. 1. From repo root: `npm run vscode:install` then `npm run vscode:build`
2. Home should end with an FAQ. Docs should show Home / Docs crumbs. 2. Open `packages/vscode` in VS Code/Cursor → Run and Debug → **Run LexAI Extension** (F5), or `npm run vscode:package` and Install from VSIX
3. Search Console → URL inspection → request indexing for `https://lexai.juankibin.space/` and `/docs`. 3. Command Palette → **LexAI: Set API Key** → pick provider → paste key
4. Select ≥10 characters in an editor → right-click → **LexAI** → Fix Grammar (or another action)
## Waiting on you ## Waiting on you
| # | Decision | Options (recommended bold) | What it unblocks | | # | Decision | Options (recommended bold) | What it unblocks |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | Deploy this SEO pass to production | **Redeploy Coolify from main** / wait | Google seeing the new titles/FAQ | | 1 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch for builders/critics |
| 2 | Request indexing for Home and Docs | **Do after deploy** / later | Faster first index | | 2 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet |
| 3 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet | | 3 | Publish VS Code extension to Marketplace? | **Stay local for now** / set publisher + publish | public install |
## Next up — proceeds without you ## Next up — proceeds without you
- Rankings take days to weeks; no extra code until the new HTML is live - Optional: VS Code Prompt Builder / style quick-pick / Marketplace packaging when you ask

View File

@@ -8,7 +8,6 @@ _No active task contracts._
## Done (recent) ## Done (recent)
- **T-WEB-SEO-01** — Google indexing prep for `packages/website` (path URLs, robots/sitemap, unique titles, 404) (2026-09-03).
- **T-VSCODE-01** — VS Code LexAI v1 (`packages/vscode`): native commands/menus, Secret Storage, shared `@lib` via esbuild; verified with `vscode:typecheck` + `vscode:build` + root typecheck/tests (2026-08-13). - **T-VSCODE-01** — VS Code LexAI v1 (`packages/vscode`): native commands/menus, Secret Storage, shared `@lib` via esbuild; verified with `vscode:typecheck` + `vscode:build` + root typecheck/tests (2026-08-13).
## Task contract format ## Task contract format

View File

@@ -1,47 +0,0 @@
# Website indexing (Google)
## User + job
Someone searching for a BYO-LLM writing/code assist tool finds LexAI and lands on the matching page (home, docs, contribute, or community). Success: the snippet title and URL match the page they open.
## Flow
Search result → HTTPS path (`/`, `/docs`, `/contribute`, `/community`) → page. Old `#/docs` links redirect to `/docs` with no extra click. Unknown paths: one screen, link home. Target: 0 taps after the SERP click.
## Screen states
| State | What |
| --- | --- |
| Success | Existing Home / Docs / Contribute / Community; browser tab title matches the route |
| Not found | H1 “Page not found”, one sentence, primary “Home”, secondary “Docs” |
| Loading / empty / offline | Unchanged marketing pages (static). No new loading chrome |
## Components
Reuse `SiteHeader`, `SiteFooter`, `.page` / `.docs-content` / `.btn`. No new design-system pattern except **Not found** (same as `.page`).
## Copy (en)
- Title (404): Page not found
- Body: That address is not a LexAI page. Check the URL or go back home.
- Primary: Home
- Secondary: Docs
Route titles/descriptions live in `src/seo.ts` (not on-page H1s).
## Accessibility
404 is a real `h1`; both actions are text links/buttons ≥ 40px; focus order follows visual order.
## Acceptance
1. `/docs`, `/contribute`, `/community` are path URLs (not `#/…`).
2. `/robots.txt` allows `/` and names the sitemap.
3. `/sitemap.xml` lists the four indexable URLs on `https://lexai.juankibin.space`.
4. Each of those URLs has a unique `<title>` and meta description in the HTML file Google fetches.
5. Unknown path shows the 404 copy; crawlers get HTTP 404.
6. `#/docs` becomes `/docs`.
## Out of scope
Search Console verification token, sitemap ping, paid ads, i18n locales beyond en.

View File

@@ -1,30 +0,0 @@
# Home FAQ (SEO)
## User + job
Someone comparing LexAI to Grammarly or Copilot gets a direct answer without leaving Home. Success: they install or open Docs.
## Flow
Home → FAQ section → expand one question → Docs or store CTA already on the page. Target: read in place (0 extra pages).
## Screen states
| State | What |
| --- | --- |
| Success | Five `<details>`; first collapsed |
| Empty / loading / error / offline | Static copy; no extra states |
## Copy
Questions and answers live in `packages/website/src/faq.json` only (visible text = JSON-LD).
## Accessibility
`summary` min-height 44px; native details/summary keyboard. Breadcrumbs: `nav aria-label="Breadcrumb"` on Docs, Contribute, Community.
## Acceptance
1. Home `#faq` shows the five Q&As from `faq.json`.
2. Built `dist/index.html` includes `FAQPage` JSON-LD with the same questions.
3. Docs/Contribute/Community show Home / current breadcrumbs.

View File

@@ -1,33 +0,0 @@
# Design spec — LexAI website
## User + job
Open-source visitors evaluating LexAI. Success: understand BYO-key privacy, pick Chrome / VS Code / CLI, reach install or docs in ≤ 2 clicks from the hero.
## Flow
Landing → Features → Packages → Docs (deep) or Chrome Web Store / Git. Secondary: Docs nav anchors per package.
## Screens
| Route | Purpose |
| --- | --- |
| `/` | Brand hero, feature highlights, package cards, privacy, CTAs |
| `/docs` | All packages + features, install pointers, shared core |
States: static marketing (no loading/error beyond 404 → home link).
## Visual
- Brand **LexAI** is the hero signal; bolt mark from product icon.
- Atmosphere: deep slate field + amber bolt accent (from icon gold), not purple-on-white.
- Type: Syne (display) + Source Sans 3 (body).
- No card grid in hero; package cards only where choosing a product is the interaction.
## Acceptance
1. First viewport: LexAI brand, one headline, one support line, CTA group, dominant visual.
2. Features of writing + Code Assist + Prompt Builder called out.
3. Docs lists Chrome, VS Code, CLI with feature bullets and links to store/repo READMEs.
4. Works desktop + mobile.
5. `npm run website:build` succeeds.

View File

@@ -28,14 +28,8 @@
"cli:install": "npm install --prefix packages/cli", "cli:install": "npm install --prefix packages/cli",
"cli:build": "npm run compile --prefix packages/cli", "cli:build": "npm run compile --prefix packages/cli",
"cli:typecheck": "npm run typecheck --prefix packages/cli", "cli:typecheck": "npm run typecheck --prefix packages/cli",
"website:install": "npm install --prefix packages/website", "install:all": "npm run chrome:install && npm run vscode:install && npm run cli:install",
"website:dev": "npm run dev --prefix packages/website", "typecheck:all": "npm run chrome:typecheck && npm run vscode:typecheck && npm run cli:typecheck",
"website:build": "npm run build --prefix packages/website", "build:all": "npm run chrome:build && npm run vscode:build && npm run cli:build"
"website:preview": "npm run preview --prefix packages/website",
"website:start": "npm start --prefix packages/website",
"website:typecheck": "npm run typecheck --prefix packages/website",
"install:all": "npm run chrome:install && npm run vscode:install && npm run cli:install && npm run website:install",
"typecheck:all": "npm run chrome:typecheck && npm run vscode:typecheck && npm run cli:typecheck && npm run website:typecheck",
"build:all": "npm run chrome:build && npm run vscode:build && npm run cli:build && npm run website:build"
} }
} }

View File

@@ -1,27 +1,17 @@
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
const libRoot = fileURLToPath(new URL('../../src/lib', import.meta.url));
const setupFile = fileURLToPath(new URL('../../tests/unit/setup.ts', import.meta.url));
export default defineConfig({ export default defineConfig({
root: repoRoot,
resolve: { resolve: {
alias: { alias: {
'@lib': libRoot, '@lib': fileURLToPath(new URL('../../src/lib', import.meta.url)),
},
},
server: {
fs: {
allow: [repoRoot],
}, },
}, },
test: { test: {
environment: 'jsdom', environment: 'jsdom',
globals: true, globals: true,
setupFiles: [setupFile], setupFiles: ['../../tests/unit/setup.ts'],
include: ['tests/unit/**/*.test.ts'], include: ['../../tests/unit/**/*.test.ts'],
exclude: ['tests/e2e/**/*', 'packages/**'], exclude: ['../../tests/e2e/**/*'],
}, },
}); });

View File

@@ -10,15 +10,7 @@
--- ---
## A. Install from the Marketplace (recommended) ## A. Install on your machine (sideload)
[LexAI on the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode) — search **LexAI** or:
```powershell
code --install-extension JuanKibin.lexai-vscode
```
## B. Install on your machine (sideload)
### VS Code ### VS Code

View File

@@ -78,7 +78,7 @@ Change in Settings or via option chips in the suggestion zone.
## Quick start ## Quick start
1. Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode), or **Install from VSIX**. 1. Install this extension (Marketplace, Open VSX, or **Install from VSIX**).
2. Command Palette → **LexAI: Open Settings** → provider, model, API key. 2. Command Palette → **LexAI: Open Settings** → provider, model, API key.
3. Select text or code → click **LexAI** on the line → pick an action or **Code Assist**. 3. Select text or code → click **LexAI** on the line → pick an action or **Code Assist**.
4. Review in the suggestion zone → Accept to replace the selection (or Copy from the sidebar / panel). 4. Review in the suggestion zone → Accept to replace the selection (or Copy from the sidebar / panel).
@@ -99,7 +99,6 @@ Install / publish details: [DEPLOY.md](./DEPLOY.md).
| | | | | |
| --- | --- | | --- | --- |
| **This extension** | [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode) (`JuanKibin.lexai-vscode`) |
| **Chrome extension** | [Chrome Web Store](https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk) | | **Chrome extension** | [Chrome Web Store](https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk) |
| **Source** | [LexAI repository](https://git.juankibin.space/kibin/LexAI) | | **Source** | [LexAI repository](https://git.juankibin.space/kibin/LexAI) |
| **Publisher** | JuanKibin | | **Publisher** | JuanKibin |

View File

@@ -1,3 +0,0 @@
node_modules
dist
*.md

View File

@@ -1,12 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

View File

@@ -1,78 +0,0 @@
# LexAI website
Marketing site for the LexAI monorepo: Home, Docs, Contribute, Community.
## Develop
```bash
# from repo root
npm run website:install
npm run website:dev
```
Or inside this package:
```bash
npm install
npm run dev
```
## Build
```bash
npm run website:build
# → packages/website/dist/
```
Preview production build locally: `npm run website:preview` (Vite, localhost only).
Production start (what Coolify should run): `npm start` — serves `dist/` on `0.0.0.0:$PORT` (default 3000).
Uses BrowserRouter. Production nginx/Node serve `/docs`, `/contribute`, and `/community` as real paths (legacy `#/docs` links rewrite on load). Unknown paths return HTTP 404.
## Deploy on Coolify
This package is a static Vite build. No LexAI API keys or env vars. Do not add `LEXAI_API_KEY`.
`Cannot GET /` means Coolify started a Node process that is not serving `dist/` (no `start` command, or "static site" pointed at the wrong folder). Fix: use one of the two setups below, then **Redeploy**.
**Required on either setup**
1. **+ New** → **Resource** → **Private Repository (with deploy key)** (Gitea).
2. Repo SSH URL, for example `git@git.juankibin.space:kibin/LexAI.git`. If Gitea SSH is not on port 22, use `ssh://git@git.juankibin.space:PORT/kibin/LexAI.git`.
3. Add Coolify’s public deploy key to the Gitea repo: **Settings → Deploy / Access Keys** (read-only).
4. Branch: `dev` or `main`.
5. **Base Directory:** `packages/website` — no leading slash. Root `npm run build` builds Chrome, not this site.
6. Optional **Watch Paths:** `packages/website`.
7. Set the domain, deploy, open `https://lexai.juankibin.space/` (docs: `/docs`).
### Option A — Dockerfile (recommended)
1. **Build Pack:** Dockerfile.
2. Leave **Is it a static site?** **off**.
3. **Ports Exposes:** `80`.
4. If Coolify asks for a Dockerfile path: `packages/website/Dockerfile` (or omit it when Base Directory is already `packages/website`).
### Option B — Nixpacks
1. **Build Pack:** Nixpacks.
2. Leave **Is it a static site?** **off**. Nixpacks runs `npm run build` then `node server.mjs`.
3. **Start command** (if the field is shown): `npm start` or `node server.mjs`.
4. **Ports Exposes:** `3000`.
5. Optional env: `NIXPACKS_NODE_VERSION=22`. Do not set `NODE_ENV=production` at **build** time — Vite is a devDependency and `npm ci` would skip it.
Do **not** tick "Is it a static site?" with Publish Directory `/dist`. The leading slash is an absolute path (`/dist` on the container), which is empty, and Coolify then answers `Cannot GET /`.
If you already created the resource: uncheck static site, set Base Directory and port as above, then Redeploy (no need to recreate the app).
Auto-deploy: Coolify resource → **Webhooks** → copy the URL + secret into Gitea **Settings → Webhooks** (push events).
## Google Search
After the site is live on `https://lexai.juankibin.space/`:
1. Open [Google Search Console](https://search.google.com/search-console), add a URL-prefix property for that origin, and verify (HTML file or DNS).
2. Sitemaps → submit `https://lexai.juankibin.space/sitemap.xml`.
3. URL inspection → request indexing for `/` and `/docs`.
Crawlers should see `Allow: /` at `/robots.txt` and unique titles on `/`, `/docs`, `/contribute`, and `/community`.

View File

@@ -1,229 +0,0 @@
# LexAI — Social media posts
Ready-to-copy posts for promoting [LexAI](https://lexai.juankibin.space/).
**Website:** https://lexai.juankibin.space/
**Chrome Web Store:** https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
**VS Code Marketplace:** https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
**Repository:** https://git.juankibin.space/kibin/LexAI
---
## Prompt Builder — featured angle
Use these when you want **Prompt Builder** front and center. It works in three places: Chrome (**Make Prompt**), VS Code, and the CLI (`lexai prompt`).
### LinkedIn — Prompt Builder focus
Bad prompts waste good models.
LexAI **Prompt Builder** turns a rough idea into a paste-ready, engineered prompt — before you send it to Claude, Cursor, ChatGPT, or any other agent.
How it works:
- **Chrome** — select any text → **Make Prompt** → copy a structured prompt for your next session
- **VS Code / Cursor** — select code or notes → Make Prompt with pattern, persona, and format options
- **CLI** — `lexai prompt "add rate limiting to the auth routes"` → engineered prompt on stdout, ready to pipe or paste
Pick a pattern (role, chain-of-thought, and more), set persona and output format, and get something you can actually hand to an AI agent.
Same BYO-key model: your API key, your provider, no LexAI servers.
Try it: https://lexai.juankibin.space/
Docs: https://lexai.juankibin.space/docs
#PromptEngineering #AI #OpenSource #DeveloperTools #BYOLLM
### X / Twitter — Prompt Builder focus
> Stop pasting vague prompts into Claude and Cursor.
>
> LexAI Prompt Builder: rough idea → engineered, paste-ready prompt.
>
> Chrome · VS Code · CLI (`lexai prompt`)
> Your key. No subscription.
>
> https://lexai.juankibin.space/
### Short demo tweet
> "add rate limiting to auth" →
>
> LexAI Prompt Builder →
>
> structured prompt you paste into Cursor ✓
>
> Browser, editor, or terminal. BYO API key.
> https://lexai.juankibin.space/
---
## LinkedIn
### Option A — Full launch post
I built LexAI because I was tired of paying for Grammarly *and* Copilot when I already had an LLM API key.
LexAI is an open-source writing, coding, and **prompt-engineering** assistant that uses **your** API key — no LexAI account, no servers, no subscription.
What it does:
- **Prompt Builder** — turn a rough idea into a paste-ready prompt (Chrome, VS Code, or CLI). Patterns, persona, and format built in.
- **Chrome extension** — select text on any webpage → Fix, Rephrase, Shorten, Expand, Explain, or **Make Prompt**
- **VS Code / Cursor extension** — select code, give an instruction, apply a suggestion with file context
- **CLI** — `lexai prompt "your idea"` outputs an engineered prompt to stdout — pipe it straight into your workflow
Your key stays on your device. Text goes only to the provider you choose (OpenAI, Anthropic, Groq, or OpenRouter).
MIT licensed. Audit it, fork it, ship your own build.
Learn more: https://lexai.juankibin.space/
Chrome: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
VS Code: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
#OpenSource #AI #PromptEngineering #DeveloperTools #Productivity #BYOLLM
### Option B — Short
Skip the second subscription — and stop writing bad prompts by hand.
LexAI = Grammarly-style writing in Chrome + Copilot-style code assist in VS Code + a **Prompt Builder** that engineers paste-ready prompts from a rough idea.
Your LLM API key. No LexAI servers. No fee.
Open source (MIT): https://lexai.juankibin.space/
#AI #OpenSource #PromptEngineering #BuildInPublic
---
## X / Twitter
### Launch tweet
LexAI: BYO-LLM writing, coding, and **prompt engineering** — no subscription, no account, no LexAI servers.
✨ Prompt Builder — rough idea → paste-ready prompt
🌐 Chrome: fix/rephrase/**make prompt** on any page
💻 VS Code: select code → instruct → apply
⌨️ CLI: `lexai prompt "your idea"`
Open source (MIT)
https://lexai.juankibin.space/
### Thread opener
Tired of paying Grammarly AND Copilot — and still writing vague prompts for Claude/Cursor?
I built LexAI: your key, your provider, your device. Writing help + code assist + **Prompt Builder**. 🧵
*(Reply with one tweet per feature — lead with Prompt Builder, then Chrome, VS Code, CLI + links)*
### Thread reply — Prompt Builder (tweet 2)
> Feature 1: **Prompt Builder**
>
> Type a rough idea → get a structured, paste-ready prompt.
>
> Use it in Chrome (Make Prompt), VS Code, or terminal:
> `lexai prompt "debug flaky tests on CI"`
>
> Pipe to clipboard. Paste into Cursor. Done.
### Short punchy
Your API key. Your provider. No LexAI cloud.
**Prompt Builder** in browser, editor, and CLI. Writing help in Chrome. Code assist in VS Code.
https://lexai.juankibin.space/ · MIT
---
## Facebook / Instagram
Meet LexAI — AI writing, coding, and **prompt engineering** without another monthly bill.
If you already have an OpenAI, Anthropic, Groq, or OpenRouter API key, LexAI puts it to work:
✨ **Prompt Builder** — rough idea → engineered, paste-ready prompt (Chrome, VS Code, or CLI)
✍️ Fix grammar and rephrase text on any website
💻 Get coding suggestions in VS Code or Cursor
🛠️ Run `lexai prompt` from the terminal and pipe the result anywhere
Stop guessing what to type into ChatGPT or Cursor. LexAI structures the prompt for you.
Your data stays yours — no LexAI account, no servers, no tracking.
Free & open source.
👉 https://lexai.juankibin.space/
#LexAI #AI #PromptEngineering #OpenSource #ProductivityHack
---
## Reddit / Hacker News
### Title
Show HN: LexAI – BYO-LLM writing help, code assist, and Prompt Builder (Chrome + VS Code + CLI)
### Body
LexAI is an open-source toolkit for people who already have an LLM API key — an alternative to Grammarly + Copilot subscriptions, with a built-in **Prompt Builder** for agent workflows.
**Prompt Builder** (the part I use most):
- Turn a rough idea into a paste-ready, engineered prompt
- Available in Chrome (**Make Prompt**), VS Code, and CLI
- Patterns (role, chain-of-thought, etc.), persona, output format, and style options
- CLI example: `lexai prompt "add rate limiting to the express auth routes"` → stdout, pipe-safe
**Also:**
- Chrome MV3 extension: select text → Fix / Rephrase / Shorten / Expand / Explain / Make Prompt
- VS Code extension: workspace-aware Code Assist (select code, instruct, apply)
- Shared provider core: OpenAI, Anthropic, Groq, OpenRouter
No LexAI backend, account, or telemetry. Keys stay on-device (encrypted in Chrome, Secret Storage in VS Code).
Site: https://lexai.juankibin.space/
Docs: https://lexai.juankibin.space/docs
Repo: https://git.juankibin.space/kibin/LexAI
Chrome: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
VS Code: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
MIT licensed. Feedback and contributions welcome.
---
## Threads / Bluesky
Built LexAI for people who already pay for an LLM API key and don't want Grammarly + Copilot subscriptions on top.
Highlight: **Prompt Builder** — rough idea in, engineered prompt out. Works in the browser (Make Prompt), VS Code, and terminal (`lexai prompt`). Paste straight into Claude or Cursor.
Also: writing help in Chrome, code assist in VS Code. Your key never hits LexAI servers.
Open source: https://lexai.juankibin.space/
---
## Hashtag reference
| Platform | Suggested tags |
| --------- | -------------- |
| LinkedIn | `#OpenSource` `#AI` `#PromptEngineering` `#DeveloperTools` `#Productivity` `#BuildInPublic` `#Privacy` |
| X | `#PromptEngineering` `#BYOLLM` `#OpenSource` `#DevTools` `#AI` |
| Instagram | `#LexAI` `#AI` `#PromptEngineering` `#OpenSource` `#Tech` `#Productivity` |
---
## Posting tips
1. **Lead with Prompt Builder** when posting to dev/AI communities — "rough idea → paste-ready prompt" is a clear, immediate win.
2. **Show a before/after** — screenshot a one-line idea next to the engineered prompt LexAI outputs; that demo converts better than feature lists.
3. **CLI clip for dev Twitter** — record `lexai prompt "..." | Set-Clipboard` (or pipe to terminal) and paste into Cursor in the next frame.
4. **Pin the website link** in your bio or first comment if the platform limits link visibility.
5. **CTA by audience** — prompt engineers → CLI + Make Prompt; developers → VS Code + repo; writers/general → Chrome Web Store.

View File

@@ -1,69 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="index, follow" />
<meta
name="description"
content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/>
<link rel="canonical" href="https://lexai.juankibin.space/" />
<meta name="theme-color" content="#f7f7f5" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="LexAI" />
<meta
property="og:title"
content="LexAI — Grammarly-style writing help with your own API key"
/>
<meta
property="og:description"
content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/>
<meta property="og:url" content="https://lexai.juankibin.space/" />
<meta property="og:image" content="https://lexai.juankibin.space/icon.svg" />
<meta property="og:image:alt" content="LexAI logo" />
<meta name="twitter:card" content="summary" />
<meta
name="twitter:title"
content="LexAI — Grammarly-style writing help with your own API key"
/>
<meta
name="twitter:description"
content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/>
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<title>LexAI — Grammarly-style writing help with your own API key</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"name": "LexAI",
"url": "https://lexai.juankibin.space/",
"description": "Bring-your-own-LLM writing and code assist for Chrome, VS Code, and the CLI."
},
{
"@type": "SoftwareApplication",
"name": "LexAI",
"applicationCategory": "BrowserApplication",
"operatingSystem": "Chrome, VS Code, Windows, macOS, Linux",
"url": "https://lexai.juankibin.space/",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}
]
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,152 +0,0 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = dirname(fileURLToPath(import.meta.url));
const distHtml = join(root, 'dist', 'index.html');
const { origin, routes } = JSON.parse(
readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'),
);
const faq = JSON.parse(readFileSync(join(root, 'src', 'faq.json'), 'utf8'));
const CHROME_STORE =
'https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk';
const VSCODE_MARKETPLACE =
'https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode';
const REPO = 'https://git.juankibin.space/kibin/LexAI';
const homeDescription = routes.find((r) => r.path === '/')?.description ?? '';
function softwareGraph() {
return {
'@type': 'SoftwareApplication',
name: 'LexAI',
applicationCategory: 'BrowserApplication',
applicationSubCategory: 'Writing assistance',
operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux',
url: `${origin}/`,
downloadUrl: [CHROME_STORE, VSCODE_MARKETPLACE],
license: `${REPO}/src/branch/main/LICENSE`,
sameAs: [REPO, CHROME_STORE, VSCODE_MARKETPLACE],
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
};
}
function jsonLd(route) {
const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`;
const graph = [
{
'@type': 'WebSite',
name: 'LexAI',
url: `${origin}/`,
description: homeDescription,
},
softwareGraph(),
];
if (route.path === '/') {
graph.push({
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
});
} else {
graph.push({
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: `${origin}/` },
{ '@type': 'ListItem', position: 2, name: route.title, item: url },
],
});
}
return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph });
}
function apply(html, { title, description, url, robots, ld }) {
let out = html;
out = out.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`);
out = out.replace(
/(<meta\s+name="description"\s+content=")[^"]*(")/,
`$1${description}$2`,
);
out = out.replace(/(<meta\s+name="robots"\s+content=")[^"]*(")/, `$1${robots}$2`);
out = out.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${title}$2`);
out = out.replace(
/(<meta\s+property="og:description"\s+content=")[^"]*(")/,
`$1${description}$2`,
);
out = out.replace(/(<meta\s+property="og:url"\s+content=")[^"]*(")/, `$1${url}$2`);
out = out.replace(/(<meta\s+name="twitter:title"\s+content=")[^"]*(")/, `$1${title}$2`);
out = out.replace(
/(<meta\s+name="twitter:description"\s+content=")[^"]*(")/,
`$1${description}$2`,
);
out = out.replace(/(<link\s+rel="canonical"\s+href=")[^"]*(")/, `$1${url}$2`);
out = out.replace(
/<script type="application\/ld\+json">[\s\S]*?<\/script>/,
`<script type="application/ld+json">${ld}</script>`,
);
return out;
}
const template = readFileSync(distHtml, 'utf8');
if (!template.includes('rel="canonical"') || !template.includes('og:title')) {
console.error('dist/index.html is missing SEO tags — check index.html');
process.exit(1);
}
const written = [];
for (const route of routes) {
const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`;
const file = route.path === '/' ? 'index.html' : `${route.path.replace(/^\//, '')}/index.html`;
const dest = join(root, 'dist', file);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(
dest,
apply(template, {
title: route.title,
description: route.description,
url,
robots: 'index, follow',
ld: jsonLd(route),
}),
);
written.push(file);
}
writeFileSync(
join(root, 'dist', '404.html'),
apply(template, {
title: 'Page not found — LexAI',
description: 'That address is not a LexAI page.',
url: `${origin}/`,
robots: 'noindex, follow',
ld: JSON.stringify({
'@context': 'https://schema.org',
'@graph': [{ '@type': 'WebSite', name: 'LexAI', url: `${origin}/` }, softwareGraph()],
}),
}),
);
written.push('404.html');
const today = new Date().toISOString().slice(0, 10);
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes
.map(
(r, i) => ` <url>
<loc>${r.path === '/' ? `${origin}/` : `${origin}${r.path}`}</loc>
<lastmod>${today}</lastmod>
<changefreq>${i < 2 ? 'weekly' : 'monthly'}</changefreq>
<priority>${r.path === '/' ? '1.0' : r.path === '/docs' ? '0.8' : '0.5'}</priority>
</url>`,
)
.join('\n')}
</urlset>
`;
writeFileSync(join(root, 'dist', 'sitemap.xml'), sitemap);
written.push('sitemap.xml');
console.log('SEO HTML written for', written.join(', '));

View File

@@ -1,37 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 'ok';
}
location = /robots.txt {
add_header Cache-Control "public, max-age=300";
try_files /robots.txt =404;
}
location = /sitemap.xml {
default_type application/xml;
add_header Cache-Control "public, max-age=300";
try_files /sitemap.xml =404;
}
location ~ ^/(docs|contribute|community)/?$ {
try_files $uri $uri/ /index.html;
}
error_page 404 =404 /404.html;
location = /404.html {
internal;
}
location / {
try_files $uri $uri/ =404;
}
}

View File

@@ -1,11 +0,0 @@
[phases.setup]
nixPkgs = ["nodejs_22"]
[phases.install]
cmds = ["npm ci"]
[phases.build]
cmds = ["npm run build"]
[start]
cmd = "node server.mjs"

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
{
"name": "lexai-website",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build && node inject-seo.mjs",
"preview": "vite preview",
"start": "node server.mjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.29.0"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^6.1.0"
}
}

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="LexAI">
<rect x="8" y="8" width="112" height="112" rx="28" fill="#4f46e5"/>
<polygon points="72,22 42,68 62,68 56,106 86,60 66,60" fill="#f5c14a"/>
</svg>

Before

Width:  |  Height:  |  Size: 269 B

View File

@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://lexai.juankibin.space/sitemap.xml

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://lexai.juankibin.space/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://lexai.juankibin.space/docs</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://lexai.juankibin.space/contribute</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://lexai.juankibin.space/community</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>

View File

@@ -1,105 +0,0 @@
import { createReadStream, existsSync, statSync } from 'node:fs';
import { createServer } from 'node:http';
import { extname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(fileURLToPath(new URL('./dist', import.meta.url)));
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST || '0.0.0.0';
const MIME = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.ico': 'image/x-icon',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.txt': 'text/plain; charset=utf-8',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.xml': 'application/xml; charset=utf-8',
};
const SPA_ROUTES = new Set(['/', '/docs', '/contribute', '/community']);
function requestPath(urlPath) {
const decoded = decodeURIComponent((urlPath ?? '/').split('?')[0] || '/');
if (decoded.length > 1 && decoded.endsWith('/')) return decoded.slice(0, -1) || '/';
return decoded || '/';
}
function isSpaRoute(urlPath) {
return SPA_ROUTES.has(requestPath(urlPath));
}
function fileUnderRoot(urlPath) {
const decoded = decodeURIComponent((urlPath ?? '/').split('?')[0] || '/');
const relative = decoded.replace(/^\/+/, '').replaceAll('/', sep);
const resolved = resolve(root, relative);
const prefix = root.endsWith(sep) ? root : root + sep;
if (resolved !== root && !resolved.startsWith(prefix)) return null;
return resolved;
}
function send(res, status, body, type = 'text/plain; charset=utf-8') {
res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'no-cache' });
res.end(body);
}
const server = createServer((req, res) => {
if (req.method === 'GET' && (req.url === '/healthz' || req.url?.startsWith('/healthz?'))) {
send(res, 200, 'ok');
return;
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
send(res, 405, 'Method not allowed');
return;
}
const candidate = fileUnderRoot(req.url ?? '/');
if (!candidate) {
send(res, 400, 'Bad request');
return;
}
let file = candidate;
let status = 200;
if (existsSync(file) && statSync(file).isDirectory()) {
const nested = join(file, 'index.html');
file = existsSync(nested) ? nested : join(root, 'index.html');
} else if (!existsSync(file)) {
const notFound = join(root, '404.html');
file = existsSync(notFound) && !isSpaRoute(req.url ?? '/') ? notFound : join(root, 'index.html');
if (!isSpaRoute(req.url ?? '/')) status = 404;
}
if (!existsSync(file)) {
send(res, 500, 'dist/ missing — run npm run build before start');
return;
}
const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream';
const headers = { 'Content-Type': type };
const base = file.replace(/\\/g, '/');
if (base.endsWith('/robots.txt') || base.endsWith('/sitemap.xml')) {
headers['Cache-Control'] = 'public, max-age=300';
}
res.writeHead(status, headers);
if (req.method === 'HEAD') {
res.end();
return;
}
createReadStream(file).pipe(res);
});
if (!existsSync(join(root, 'index.html'))) {
console.error('dist/index.html missing — run npm run build before start');
process.exit(1);
}
server.listen(port, host, () => {
console.log(`LexAI website listening on http://${host}:${port}`);
});

View File

@@ -1,28 +0,0 @@
import { Route, Routes } from 'react-router-dom';
import { SeoHead } from './components/SeoHead';
import { SiteHeader } from './components/SiteHeader';
import { SiteFooter } from './components/SiteFooter';
import { HomePage } from './pages/HomePage';
import { DocsPage } from './pages/DocsPage';
import { ContributePage } from './pages/ContributePage';
import { CommunityPage } from './pages/CommunityPage';
import { NotFoundPage } from './pages/NotFoundPage';
export default function App() {
return (
<div className="shell">
<SeoHead />
<SiteHeader />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/contribute" element={<ContributePage />} />
<Route path="/community" element={<CommunityPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
<SiteFooter />
</div>
);
}

View File

@@ -1,24 +0,0 @@
import { Link } from 'react-router-dom';
type Crumb = { label: string; to?: string };
export function Breadcrumbs({ items }: { items: Crumb[] }) {
return (
<nav className="crumbs" aria-label="Breadcrumb">
<ol>
{items.map((item, i) => {
const last = i === items.length - 1;
return (
<li key={item.label}>
{last || !item.to ? (
<span aria-current={last ? 'page' : undefined}>{item.label}</span>
) : (
<Link to={item.to}>{item.label}</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}

View File

@@ -1,21 +0,0 @@
type LogoMarkProps = {
size?: number;
className?: string;
};
/** Flat LexAI mark — indigo tile + gold bolt, for the light site. */
export function LogoMark({ size = 28, className }: LogoMarkProps) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 128 128"
width={size}
height={size}
aria-hidden="true"
>
<rect x="8" y="8" width="112" height="112" rx="28" fill="#4f46e5" />
<polygon points="72,22 42,68 62,68 56,106 86,60 66,60" fill="#f5c14a" />
</svg>
);
}

View File

@@ -1,56 +0,0 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { canonicalUrl, jsonLdForPath, seoForPath, SITE_ORIGIN } from '../seo';
function upsertMeta(selector: string, attrs: Record<string, string>, content: string) {
let el = document.head.querySelector(selector) as HTMLMetaElement | null;
if (!el) {
el = document.createElement('meta');
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
document.head.appendChild(el);
}
el.setAttribute('content', content);
}
function upsertCanonical(href: string) {
let el = document.head.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
if (!el) {
el = document.createElement('link');
el.rel = 'canonical';
document.head.appendChild(el);
}
el.href = href;
}
export function SeoHead() {
const { pathname } = useLocation();
useEffect(() => {
const seo = seoForPath(pathname);
const url = seo.index ? canonicalUrl(seo.path) : `${SITE_ORIGIN}${pathname}`;
document.title = seo.title;
upsertMeta('meta[name="description"]', { name: 'description' }, seo.description);
upsertMeta(
'meta[name="robots"]',
{ name: 'robots' },
seo.index ? 'index, follow' : 'noindex, follow',
);
upsertCanonical(url);
upsertMeta('meta[property="og:title"]', { property: 'og:title' }, seo.title);
upsertMeta('meta[property="og:description"]', { property: 'og:description' }, seo.description);
upsertMeta('meta[property="og:url"]', { property: 'og:url' }, url);
upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, seo.title);
upsertMeta('meta[name="twitter:description"]', { name: 'twitter:description' }, seo.description);
let ld = document.head.querySelector(
'script[type="application/ld+json"]',
) as HTMLScriptElement | null;
if (!ld) {
ld = document.createElement('script');
ld.type = 'application/ld+json';
document.head.appendChild(ld);
}
ld.textContent = JSON.stringify(jsonLdForPath(pathname));
}, [pathname]);
return null;
}

View File

@@ -1,21 +0,0 @@
import { Link } from 'react-router-dom';
import { REPO } from '../links';
export function SiteFooter() {
return (
<footer className="footer">
<div className="wrap footer-inner">
<p style={{ margin: 0 }}>© {new Date().getFullYear()} LexAI · MIT</p>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Link to="/docs">Docs</Link>
<Link to={{ pathname: '/', hash: 'faq' }}>FAQ</Link>
<Link to="/contribute">Contribute</Link>
<Link to="/community">Community</Link>
<a href={REPO} target="_blank" rel="noreferrer">
Source
</a>
</div>
</div>
</footer>
);
}

View File

@@ -1,28 +0,0 @@
import { Link, NavLink } from 'react-router-dom';
import { LogoMark } from './LogoMark';
import { REPO } from '../links';
export function SiteHeader() {
return (
<header className="header">
<div className="wrap header-inner">
<Link to="/" className="brand" aria-label="LexAI home">
<LogoMark size={22} />
LexAI
</Link>
<nav className="nav" aria-label="Primary">
<NavLink to="/docs">Docs</NavLink>
<NavLink to="/contribute" className="hide-sm">
Contribute
</NavLink>
<NavLink to="/community" className="hide-sm">
Community
</NavLink>
<a className="btn btn-primary" href={REPO} target="_blank" rel="noreferrer">
Star on Gitea
</a>
</nav>
</div>
</header>
);
}

View File

@@ -1,22 +0,0 @@
[
{
"q": "Is LexAI free?",
"a": "Yes. LexAI has no subscription and no account. You only pay the LLM provider you choose. Groq and OpenRouter have free-tier keys you can use to start."
},
{
"q": "Do I need my own API key?",
"a": "Yes. LexAI is bring-your-own-LLM: paste a key for OpenAI, Anthropic, Groq, or OpenRouter in Chrome Options or VS Code settings. There is no LexAI backend."
},
{
"q": "Is LexAI a Grammarly alternative?",
"a": "For writing help, yes: select text on any webpage and Fix, Rephrase, Shorten, Expand, or Explain. Unlike Grammarly, your key stays on your device and text goes only to the provider you pick."
},
{
"q": "How is LexAI different from GitHub Copilot?",
"a": "In VS Code or Cursor you select code, give an instruction, and apply a suggestion with file context. It is not tab-complete autocomplete. You use your own provider key instead of a Copilot subscription."
},
{
"q": "Does LexAI store my text or API key?",
"a": "No LexAI servers sit in the path. The Chrome key is stored on your device; VS Code uses Secret Storage. Text is sent only to the provider endpoint you configure. There is no LexAI telemetry."
}
]

View File

@@ -1,121 +0,0 @@
export type FreeTierBadge = 'best' | 'no-card' | 'trial';
export type FreeProvider = {
id: 'groq' | 'openrouter' | 'anthropic' | 'openai';
name: string;
badge?: FreeTierBadge;
badgeLabel?: string;
summary: string;
creditCard: string;
limits: string;
lexaiModel: string;
signupUrl: string;
docsUrl: string;
steps: string[];
};
export const FREE_PROVIDER_BADGES: Record<FreeTierBadge, string> = {
best: 'Best free option',
'no-card': 'No card needed',
trial: 'Trial credits',
};
/** How to use LexAI at $0 — provider free tiers as of 2026. Limits change; verify on each site. */
export const FREE_PROVIDERS: FreeProvider[] = [
{
id: 'groq',
name: 'Groq',
badge: 'best',
badgeLabel: 'Best free option',
summary:
'Ongoing free API access with no credit card. Rate limits apply, but there is no per-token charge on the free tier — ideal for daily writing help and light coding assist.',
creditCard: 'Not required',
limits: 'Rate-limited (roughly 30 requests/min; daily caps vary by model). Check the Groq console for your org.',
lexaiModel: 'llama-3.3-70b-versatile',
signupUrl: 'https://console.groq.com/keys',
docsUrl: 'https://console.groq.com/docs/overview',
steps: [
'Create a Groq account at console.groq.com — no payment method needed.',
'Generate an API key under API Keys.',
'In LexAI Options (Chrome), Settings (VS Code), or LEXAI_PROVIDER=groq (CLI): choose Groq, paste the key.',
'Optional: set model to llama-3.1-8b-instant for faster, lighter requests on tighter limits.',
],
},
{
id: 'openrouter',
name: 'OpenRouter',
badge: 'no-card',
badgeLabel: 'No card needed',
summary:
'25+ models at $0 per token via the :free suffix or the openrouter/free router. One API key covers many models — good for experimenting before you commit to a host.',
creditCard: 'Not required for free models',
limits:
'About 20 requests/min. ~50 free requests/day without credits; ~1,000/day after a one-time $10 credit purchase (credits never expire).',
lexaiModel: 'openrouter/free',
signupUrl: 'https://openrouter.ai/settings/keys',
docsUrl: 'https://openrouter.ai/docs/guides/routing/model-variants/free',
steps: [
'Sign up at openrouter.ai and create an API key.',
'In LexAI, choose OpenRouter and paste the key.',
'Set model to openrouter/free (auto-picks a free model) or a specific slug such as meta-llama/llama-3.3-70b-instruct:free.',
'Optional: add $10 in credits once to raise the daily free-model cap to ~1,000 requests — still $0 per token on free routes.',
],
},
{
id: 'anthropic',
name: 'Anthropic',
badge: 'trial',
badgeLabel: 'Trial credits',
summary:
'New Console accounts receive a small one-time API credit (often around $5) with no card required to claim. Enough to try Claude-powered writing and Code Assist before prepaid billing.',
creditCard: 'Not required to claim starter credit',
limits: 'One-time credit balance; then pay-as-you-go. Use Haiku models to stretch the trial further.',
lexaiModel: 'claude-3-5-haiku-20241022',
signupUrl: 'https://console.anthropic.com/',
docsUrl: 'https://docs.anthropic.com/en/api/getting-started',
steps: [
'Create an account at console.anthropic.com and claim the new-user API credit.',
'Create an API key under API Keys.',
'In LexAI, choose Anthropic, paste the key, and keep the default Haiku model for lowest cost.',
'When credits run out, add prepaid balance or switch to Groq or OpenRouter for ongoing free usage.',
],
},
{
id: 'openai',
name: 'OpenAI',
badge: 'trial',
badgeLabel: 'Limited trial',
summary:
'No permanent free API tier. Some new accounts still receive a short-lived trial credit; otherwise a minimum prepaid purchase is required. Mentioned for completeness — Groq or OpenRouter are better $0 paths.',
creditCard: 'Required for ongoing use; trial availability varies',
limits: 'Trial credits (if offered) expire after a few months. Prepaid billing from about $5 minimum after trial.',
lexaiModel: 'gpt-4o-mini',
signupUrl: 'https://platform.openai.com/api-keys',
docsUrl: 'https://platform.openai.com/docs/guides/rate-limits',
steps: [
'Create an account at platform.openai.com and check Billing for any trial balance.',
'If trial credit is available, create an API key and use it in LexAI with provider OpenAI.',
'Prefer gpt-4o-mini to conserve tokens during evaluation.',
'For sustained free usage, use Groq or OpenRouter instead — OpenAI is pay-as-you-go after any trial.',
],
},
];
export const LEXAI_FREE_HIGHLIGHTS = [
{
title: 'LexAI is always $0',
body: 'No LexAI subscription, account, or backend. You only connect a provider API key you control.',
},
{
title: 'Same key, three apps',
body: 'Chrome extension, VS Code / Cursor, and CLI Prompt Builder all use the same supported providers.',
},
{
title: 'Start with Groq or OpenRouter',
body: 'Both offer real ongoing free API access without a credit card — enough for grammar fixes, rewrites, and light Code Assist.',
},
{
title: 'Your key stays local',
body: 'Keys are encrypted in Chrome, stored in VS Code Secret Storage, or read from env in the CLI — never sent to LexAI.',
},
] as const;

View File

@@ -1,9 +0,0 @@
export const CHROME_STORE =
'https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk';
export const VSCODE_MARKETPLACE =
'https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode';
export const REPO = 'https://git.juankibin.space/kibin/LexAI';
export const REPO_ISSUES = `${REPO}/issues/new`;
export const REPO_CONTRIBUTING = `${REPO}/src/branch/main/CONTRIBUTING.md`;
export const REPO_CONDUCT = `${REPO}/src/branch/main/CODE_OF_CONDUCT.md`;
export const REPO_LICENSE = `${REPO}/src/branch/main/LICENSE`;

View File

@@ -1,19 +0,0 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles.css';
const hash = window.location.hash;
if (hash.startsWith('#/')) {
const path = hash.slice(1) || '/';
window.history.replaceState(null, '', path + window.location.search);
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);

View File

@@ -1,85 +0,0 @@
import { Link } from 'react-router-dom';
import { Breadcrumbs } from '../components/Breadcrumbs';
import {
CHROME_STORE,
REPO,
REPO_CONDUCT,
REPO_ISSUES,
REPO_LICENSE,
VSCODE_MARKETPLACE,
} from '../links';
export function CommunityPage() {
return (
<div className="wrap page">
<article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Community' }]} />
<h1>Community</h1>
<p className="intro">
LexAI is maintained in the open. Use these links to install, report issues, and follow the
project.
</p>
<section className="docs-block" id="source">
<h2>Source &amp; discussion</h2>
<div className="package-grid">
<a className="package-card" href={REPO} target="_blank" rel="noreferrer">
<strong>Repository</strong>
<p>Source, issues, and pull requests on Gitea.</p>
<span className="more">git.juankibin.space/kibin/LexAI</span>
</a>
<a className="package-card" href={REPO_ISSUES} target="_blank" rel="noreferrer">
<strong>Bug report</strong>
<p>Use the issue template. Redact API keys.</p>
<span className="more">Open an issue</span>
</a>
<a className="package-card" href={REPO_CONDUCT} target="_blank" rel="noreferrer">
<strong>Code of Conduct</strong>
<p>Contributor Covenant. Be kind and stay on topic.</p>
<span className="more">Read</span>
</a>
</div>
</section>
<section className="docs-block" id="install">
<h2>Install channels</h2>
<ul>
<li>
<a href={CHROME_STORE} target="_blank" rel="noreferrer">
Chrome Web Store
</a>
</li>
<li>
<a href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
VS Code Marketplace
</a>{' '}
— <code>JuanKibin.lexai-vscode</code>
</li>
<li>
CLI — build from the repo (<Link to="/docs">docs</Link>)
</li>
</ul>
</section>
<section className="docs-block" id="audience">
<h2>Who it’s for</h2>
<p>
People who already hold an LLM API key and want inline writing or prompt help without a
SaaS subscription. Developers using Chrome, VS Code / Cursor, or a terminal agent loop.
</p>
</section>
<section className="docs-block" id="license">
<h2>License</h2>
<p>
LexAI is released under the{' '}
<a href={REPO_LICENSE} target="_blank" rel="noreferrer">
MIT License
</a>
.
</p>
</section>
</article>
</div>
);
}

View File

@@ -1,90 +0,0 @@
import { Breadcrumbs } from '../components/Breadcrumbs';
import { REPO, REPO_CONDUCT, REPO_CONTRIBUTING } from '../links';
export function ContributePage() {
return (
<div className="wrap page">
<article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Contribute' }]} />
<h1>Contribute</h1>
<p className="intro">
LexAI is a monorepo: Chrome, VS Code, CLI, website, and a shared library. Please follow
the{' '}
<a href={REPO_CONDUCT} target="_blank" rel="noreferrer">
Code of Conduct
</a>
.
</p>
<section className="docs-block" id="ways">
<h2>Ways to help</h2>
<ul>
<li>Bug reports with reproduction steps</li>
<li>Documentation fixes</li>
<li>Small, focused pull requests (one concern per PR)</li>
<li>Design discussion when a change is large or security-sensitive</li>
</ul>
</section>
<section className="docs-block" id="setup">
<h2>Development setup</h2>
<p>Node.js 22+ and npm 11+.</p>
<pre className="mono">{`git clone https://git.juankibin.space/kibin/LexAI.git
cd LexAI
npm run install:all`}</pre>
<ul>
<li>
<code>npm run chrome:dev</code> / <code>chrome:test</code> /{' '}
<code>chrome:typecheck</code>
</li>
<li>
<code>npm run vscode:build</code> / <code>vscode:typecheck</code>
</li>
<li>
<code>npm run cli:build</code> / <code>cli:typecheck</code>
</li>
<li>
<code>npm run website:dev</code>
</li>
</ul>
<p>
Shared code lives in <code>src/lib/</code>. Do not import <code>@lib/crypto</code> or{' '}
<code>@lib/messaging</code> from VS Code or CLI.
</p>
</section>
<section className="docs-block" id="prs">
<h2>Pull requests</h2>
<ol>
<li>
Branch from <code>main</code> or <code>develop</code>.
</li>
<li>Keep the change scoped. Update docs when behavior changes.</li>
<li>Run the checks for the package you touched.</li>
<li>
Use the PR template in <code>.gitea/PULL_REQUEST_TEMPLATE.md</code>.
</li>
<li>Never commit API keys, tokens, or personal data.</li>
</ol>
</section>
<section className="docs-block" id="security">
<h2>Security</h2>
<p>
If you find a vulnerability around API-key handling or data exfiltration, do not open a
public issue with exploit details. Contact the maintainer privately (repository owner /
Chrome Web Store listing).
</p>
<div className="docs-meta">
<a className="btn btn-primary" href={REPO} target="_blank" rel="noreferrer">
Open the repository
</a>
<a className="btn btn-ghost" href={REPO_CONTRIBUTING} target="_blank" rel="noreferrer">
CONTRIBUTING.md
</a>
</div>
</section>
</article>
</div>
);
}

View File

@@ -1,367 +0,0 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Breadcrumbs } from '../components/Breadcrumbs';
import { FREE_PROVIDERS } from '../freeProviders';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links';
function scrollToSection(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
export function DocsPage() {
useEffect(() => {
const fromPath = window.location.href.match(/docs[#/]+([\w-]+)/i)?.[1];
const target = fromPath && fromPath !== 'docs' ? fromPath : undefined;
if (target) {
window.setTimeout(() => scrollToSection(target), 80);
}
}, []);
const nav = [
['overview', 'Overview'],
['free', 'Free LLM setup'],
['chrome', 'Chrome'],
['vscode', 'VS Code'],
['cli', 'CLI'],
['shared', 'Shared core'],
['install', 'Install'],
['privacy', 'Privacy'],
] as const;
return (
<div className="wrap docs-layout">
<aside className="docs-nav" aria-label="Docs sections">
{nav.map(([id, label]) => (
<button key={id} type="button" className="nav-link-btn" onClick={() => scrollToSection(id)}>
{label}
</button>
))}
</aside>
<article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Docs' }]} />
<h1>Documentation</h1>
<p className="intro">
LexAI is an open-source, bring-your-own-LLM assistant. This page covers every package and
its features. Contributor setup lives on the{' '}
<Link to="/contribute">Contribute</Link> page.
</p>
<section className="docs-block" id="overview">
<h2>Overview</h2>
<p>
Three clients share one provider/prompt core. There is no LexAI backend — you supply an
API key for OpenAI, Anthropic, Groq, or OpenRouter.
</p>
<ul>
<li>
<strong>Chrome</strong> — writing help on any webpage
</li>
<li>
<strong>VS Code / Cursor</strong> — writing actions + workspace-aware Code Assist
</li>
<li>
<strong>CLI</strong> — Prompt Builder for the terminal
</li>
</ul>
</section>
<section className="docs-block" id="free">
<h2>Free LLM setup</h2>
<p>
LexAI is free — no subscription, no LexAI account. To use it at $0 ongoing, connect a
provider that offers a free API tier. Groq and OpenRouter are the most practical; Anthropic
and OpenAI offer small trial credits but not a permanent free API.
</p>
<h3>Recommended path</h3>
<ol>
<li>
Install LexAI (Chrome, VS Code, or CLI) — see <a href="#install">Install</a>.
</li>
<li>
Sign up for <a href="https://console.groq.com/keys" target="_blank" rel="noreferrer">Groq</a>{' '}
(no credit card) or{' '}
<a href="https://openrouter.ai/settings/keys" target="_blank" rel="noreferrer">
OpenRouter
</a>{' '}
(no card for free models).
</li>
<li>Create an API key on that provider&apos;s dashboard.</li>
<li>
In LexAI: choose the provider, paste the key, and set the model slug below (Chrome
Options → Model; VS Code Settings → Model; CLI{' '}
<code>LEXAI_MODEL</code>).
</li>
<li>Select text and run an action — requests go directly to your provider, not LexAI.</li>
</ol>
<h3>Providers</h3>
{FREE_PROVIDERS.map((provider) => (
<div key={provider.id} className="docs-provider">
<h4>{provider.name}</h4>
<p>{provider.summary}</p>
<ul>
<li>
<strong>Credit card:</strong> {provider.creditCard}
</li>
<li>
<strong>Limits:</strong> {provider.limits}
</li>
<li>
<strong>Suggested LexAI model:</strong> <code>{provider.lexaiModel}</code>
</li>
</ul>
<p>
<strong>Setup</strong>
</p>
<ol>
{provider.steps.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
<div className="docs-meta">
<a className="btn btn-ghost" href={provider.signupUrl} target="_blank" rel="noreferrer">
Get API key
</a>
<a className="btn btn-ghost" href={provider.docsUrl} target="_blank" rel="noreferrer">
Provider docs
</a>
</div>
</div>
))}
<h3>CLI example (Groq, free tier)</h3>
<pre className="mono">{`# PowerShell
$env:LEXAI_API_KEY = "gsk_..."
$env:LEXAI_PROVIDER = "groq"
$env:LEXAI_MODEL = "llama-3.3-70b-versatile"
lexai prompt "rewrite this email to sound more professional"
# bash
export LEXAI_API_KEY=gsk_...
export LEXAI_PROVIDER=groq
export LEXAI_MODEL=llama-3.3-70b-versatile
lexai prompt "summarize this README"`}</pre>
<h3>CLI example (OpenRouter, free models)</h3>
<pre className="mono">{`export LEXAI_API_KEY=sk-or-...
export LEXAI_PROVIDER=openrouter
export LEXAI_MODEL=openrouter/free
lexai prompt "turn this bullet list into a user story"`}</pre>
<p>
Limits and trial amounts change. Check each provider&apos;s pricing and rate-limit pages
before relying on a free tier in production.
</p>
</section>
<section className="docs-block" id="chrome">
<h2>Chrome extension</h2>
<p>
Manifest V3. Select text on any page, run an action, Replace or Copy. API key encrypted
locally with tweetnacl secretbox.
</p>
<h3>Setup</h3>
<p>
Options page → choose provider (OpenAI, Anthropic, Groq, or OpenRouter) and paste your
API key. There are no environment variables.
</p>
<h3>Features</h3>
<ul>
<li>Fix Grammar, Rephrase, Shorten, Expand, Explain, Make Prompt</li>
<li>Writing styles: Formal, Casual, Academic, Creative, Concise</li>
<li>Floating toolbar, right-click context menu, popup editor</li>
<li>Prompt Builder — patterns, persona, output format</li>
<li>Providers: OpenAI · Anthropic · Groq · OpenRouter</li>
<li>Works in text boxes, email composers, forms, and contenteditable pages</li>
</ul>
<div className="docs-meta">
<a className="btn btn-primary" href={CHROME_STORE} target="_blank" rel="noreferrer">
Chrome Web Store
</a>
<a
className="btn btn-ghost"
href={`${REPO}/src/branch/main/packages/chrome`}
target="_blank"
rel="noreferrer"
>
Package README
</a>
</div>
</section>
<section className="docs-block" id="vscode">
<h2>VS Code / Cursor extension</h2>
<p>
Editor twin of LexAI. Select text or code → action or Code Assist → Accept / Regenerate
/ Discard. Key lives in VS Code Secret Storage.
</p>
<p>
Published listing:{' '}
<a href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
JuanKibin.lexai-vscode
</a>
</p>
<h3>Setup</h3>
<p>
Command Palette → <code>LexAI: Open Settings</code> → provider, model, and API key.
The key is stored in VS Code Secret Storage. There are no environment variables.
</p>
<h3>Code Assist</h3>
<ul>
<li>Freeform instruction in an inline prompt above the selection</li>
<li>
Workspace context: surrounding code, imports, and symbol definitions from other files
</li>
<li>Entry points: CodeLens, context menu, Command Palette, Activity Bar sidebar</li>
</ul>
<h3>Writing actions</h3>
<ul>
<li>Fix Grammar · Rephrase · Shorten · Expand · Explain · Make Prompt</li>
<li>Writing styles + Prompt Builder option chips in the suggestion zone</li>
<li>Suggestion UI: zone (default), side panel, or both</li>
<li>Status bar: ready / processing / not configured</li>
<li>Sidebar workspace panel for paste → run → Copy</li>
</ul>
<div className="docs-meta">
<a className="btn btn-primary" href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
VS Code Marketplace
</a>
<a
className="btn btn-ghost"
href={`${REPO}/src/branch/main/packages/vscode`}
target="_blank"
rel="noreferrer"
>
Package README
</a>
</div>
</section>
<section className="docs-block" id="cli">
<h2>CLI — Prompt Builder</h2>
<p>
Improves a rough idea into a paste-ready engineered prompt before you send it to another
agent.
</p>
<h3>Setup</h3>
<p>Environment variables (PowerShell shown; bash uses <code>export NAME=value</code>):</p>
<pre className="mono">{`$env:LEXAI_API_KEY = "sk-..." # required
$env:LEXAI_PROVIDER = "openai" # optional: openai | anthropic | groq | openrouter
$env:LEXAI_MODEL = "gpt-4o" # optional`}</pre>
<p>
Optional defaults file (never put the API key here): <code>~/.lexai/config.json</code>
</p>
<pre className="mono">{`{
"provider": "openai",
"model": "gpt-4o",
"pattern": "role",
"persona": "Expert Developer",
"format": "Markdown",
"style": "Concise"
}`}</pre>
<p>
Precedence: flags → config file → env → defaults. The key is always{' '}
<code>LEXAI_API_KEY</code>.
</p>
<h3>Features</h3>
<ul>
<li>
Command: <code>lexai prompt</code>
</li>
<li>
Input from args, stdin, or <code>-f/--file</code>
</li>
<li>Flags: pattern, persona, format, style, provider, model</li>
<li>
Also <code>--list</code> (patterns, personas, formats, styles) and{' '}
<code>--help</code>
</li>
<li>Engineered prompt on stdout (pipe-safe); progress and errors on stderr</li>
</ul>
<h3>Examples</h3>
<pre className="mono">{`lexai prompt "add rate limiting to the express auth routes"
echo "debug flaky playwright on CI" | lexai prompt --pattern role
lexai prompt -f draft.txt --format Markdown --model gpt-4o`}</pre>
<div className="docs-meta">
<a
className="btn btn-ghost"
href={`${REPO}/src/branch/main/packages/cli`}
target="_blank"
rel="noreferrer"
>
Package README
</a>
</div>
</section>
<section className="docs-block" id="shared">
<h2>Shared core</h2>
<ul>
<li>
Location: <code>src/lib</code> (<code>providers</code>, <code>actions</code>,{' '}
<code>types</code>)
</li>
<li>Chrome-only modules: crypto (key encryption), messaging helpers</li>
<li>VS Code and CLI must not import Chrome-only modules</li>
</ul>
</section>
<section className="docs-block" id="install">
<h2>Install</h2>
<p>Published builds:</p>
<ul>
<li>
Chrome —{' '}
<a href={CHROME_STORE} target="_blank" rel="noreferrer">
Chrome Web Store
</a>
</li>
<li>
VS Code / Cursor —{' '}
<a href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
Visual Studio Marketplace
</a>{' '}
(<code>JuanKibin.lexai-vscode</code>)
</li>
</ul>
<p style={{ marginTop: '1rem' }}>From source (Node.js 22+):</p>
<pre className="mono">{`git clone https://git.juankibin.space/kibin/LexAI.git
cd LexAI
npm run install:all
# Chrome
npm run chrome:dev # or chrome:build → load packages/chrome/.output/chrome-mv3
# VS Code
npm run vscode:build # or install from Marketplace
# CLI
npm run cli:build
export LEXAI_API_KEY=sk-... # required
export LEXAI_PROVIDER=openai # optional: openai | anthropic | groq | openrouter
export LEXAI_MODEL=gpt-4o # optional
node packages/cli/out/cli.js prompt "your rough idea"
# Website
npm run website:dev`}</pre>
</section>
<section className="docs-block" id="privacy">
<h2>Privacy</h2>
<ul>
<li>No LexAI backend, account, or subscription</li>
<li>No LexAI telemetry</li>
<li>
Chrome: encrypted key in <code>chrome.storage.local</code>
</li>
<li>VS Code: Secret Storage</li>
<li>
CLI: <code>LEXAI_API_KEY</code> required; optional <code>LEXAI_PROVIDER</code> and{' '}
<code>LEXAI_MODEL</code>. Never store the key in <code>~/.lexai/config.json</code>
</li>
<li>Text is sent only to the LLM provider you configure</li>
</ul>
</section>
</article>
</div>
);
}

View File

@@ -1,256 +0,0 @@
import { Link, useNavigate } from 'react-router-dom';
import { LogoMark } from '../components/LogoMark';
import { FREE_PROVIDERS, FREE_PROVIDER_BADGES, LEXAI_FREE_HIGHLIGHTS } from '../freeProviders';
import faq from '../faq.json';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links';
function openDocsSection(navigate: ReturnType<typeof useNavigate>, id: string) {
navigate('/docs');
window.setTimeout(() => {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 60);
}
const FEATURES = [
{
title: 'Your key. Your provider.',
body: 'OpenAI, Anthropic, Groq, or OpenRouter. No LexAI account or servers.',
},
{
title: 'Writing help, like Grammarly',
body: 'Select text on any page → Fix, Rephrase, Shorten, Expand, Explain, or Make Prompt.',
},
{
title: 'Coding assist, like Copilot',
body: 'VS Code and Cursor: select code, give an instruction, apply a suggestion with file context.',
},
{
title: 'Prompt Builder',
body: (
<>
Turn a rough idea into a paste-ready prompt — in the app or via <code>lexai prompt</code>.
</>
),
},
{
title: 'Private by design',
body: 'Keys stay on your device. Text goes only to the provider you choose.',
},
{
title: 'Open source (MIT)',
body: 'Chrome, VS Code, and CLI share one core. Audit it, fork it, ship your own build.',
},
];
export function HomePage() {
const navigate = useNavigate();
return (
<>
<section className="hero">
<div className="wrap">
<div className="hero-lockup">
<LogoMark size={48} />
<p className="hero-wordmark">
Lex<span>AI</span>
</p>
</div>
<p className="hero-kicker">Bring your own LLM</p>
<h1>Skip Grammarly and Copilot subscriptions</h1>
<p className="hero-lead">
Writing help on any webpage. Coding assist in VS Code or Cursor. You bring the API key —
LexAI has no account, no servers, and no fee. Use Groq or OpenRouter for ongoing free
API access.
</p>
<div className="cta-row">
<a className="btn btn-primary" href={CHROME_STORE} target="_blank" rel="noreferrer">
Chrome Web Store
</a>
<a className="btn btn-ghost" href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
VS Code Marketplace
</a>
<a className="btn btn-ghost" href={REPO} target="_blank" rel="noreferrer">
Repository
</a>
<Link className="btn btn-ghost" to="/docs">
Docs
</Link>
</div>
</div>
</section>
<section className="section" id="why">
<div className="wrap">
<p className="section-kicker">Why try it</p>
<h2>The jobs you already pay for — without a second subscription</h2>
<p className="section-lead">
Same idea as the tools you know. You pay your LLM provider, not LexAI.
</p>
<div className="compare-grid">
<div className="package-card">
<strong>Instead of Grammarly</strong>
<p>
Fix grammar, rephrase, shorten, expand, or explain on any webpage. Your key stays on
your device.
</p>
<span className="more">Chrome extension</span>
</div>
<div className="package-card">
<strong>Instead of GitHub Copilot</strong>
<p>
Select code, type what you want, and apply a suggestion with context from other
files. Not tab-complete — instruct and review.
</p>
<span className="more">VS Code / Cursor</span>
</div>
</div>
</div>
</section>
<section className="section" id="free">
<div className="wrap">
<p className="section-kicker">Use it free</p>
<h2>LexAI costs nothing — your provider can too</h2>
<p className="section-lead">
LexAI never charges a subscription. Connect a free-tier API key from a supported provider
and get Grammarly-style writing help and Copilot-style coding assist without paying
Grammarly or Copilot.
</p>
<ul className="free-highlights">
{LEXAI_FREE_HIGHLIGHTS.map((item) => (
<li key={item.title}>
<strong>{item.title}</strong>
<p>{item.body}</p>
</li>
))}
</ul>
<div className="provider-grid">
{FREE_PROVIDERS.map((provider) => (
<article key={provider.id} className="provider-card">
<div className="provider-card-head">
<strong>{provider.name}</strong>
{provider.badge ? (
<span className={`provider-badge provider-badge-${provider.badge}`}>
{provider.badgeLabel ?? FREE_PROVIDER_BADGES[provider.badge]}
</span>
) : null}
</div>
<p>{provider.summary}</p>
<dl className="provider-meta">
<div>
<dt>Credit card</dt>
<dd>{provider.creditCard}</dd>
</div>
<div>
<dt>LexAI model</dt>
<dd>
<code>{provider.lexaiModel}</code>
</dd>
</div>
</dl>
<a
className="more"
href={provider.signupUrl}
target="_blank"
rel="noreferrer"
>
Get API key →
</a>
</article>
))}
</div>
<p className="free-footnote">
Rate limits and trial amounts change — verify on each provider&apos;s site. Full setup
steps are in{' '}
<Link to="/docs#free">Free LLM setup</Link>.
</p>
</div>
</section>
<section className="section" id="features">
<div className="wrap">
<p className="section-kicker">Product</p>
<h2>What LexAI does</h2>
<p className="section-lead">
One family across browser, editor, and CLI. Same providers. No LexAI backend.
</p>
<ul className="feature-list">
{FEATURES.map((f) => (
<li key={f.title}>
<h3>{f.title}</h3>
<p>{f.body}</p>
</li>
))}
</ul>
</div>
</section>
<section className="section" id="start">
<div className="wrap">
<p className="section-kicker">Getting started</p>
<h2>Install in minutes</h2>
<p className="section-lead">Published builds, or clone the repo if you want to develop.</p>
<div className="package-grid">
<a className="package-card" href={CHROME_STORE} target="_blank" rel="noreferrer">
<strong>Chrome</strong>
<p>Install from the Chrome Web Store. Set provider and API key in Options.</p>
<span className="more">Chrome Web Store</span>
</a>
<a className="package-card" href={VSCODE_MARKETPLACE} target="_blank" rel="noreferrer">
<strong>VS Code</strong>
<p>
Install <code>JuanKibin.lexai-vscode</code>. Open Settings, add your key, select
code.
</p>
<span className="more">Marketplace</span>
</a>
<button
type="button"
className="package-card"
onClick={() => openDocsSection(navigate, 'cli')}
>
<strong>CLI</strong>
<p>
Build <code>lexai prompt</code>, set <code>LEXAI_API_KEY</code> and optional{' '}
<code>LEXAI_PROVIDER</code> / <code>LEXAI_MODEL</code>.
</p>
<span className="more">CLI docs</span>
</button>
</div>
</div>
</section>
<section className="section">
<div className="wrap privacy">
<p className="section-kicker">Privacy</p>
<h2>No LexAI cloud</h2>
<ul>
<li>Your API key never leaves your device except to the provider you configure.</li>
<li>No LexAI telemetry and no LexAI servers in the path.</li>
<li>
Open source — inspect the shared core in <code>src/lib</code>.
</li>
</ul>
</div>
</section>
<section className="section" id="faq">
<div className="wrap">
<p className="section-kicker">FAQ</p>
<h2>Questions</h2>
<p className="section-lead">
Short answers for people comparing LexAI to Grammarly, Copilot, and hosted writing tools.
</p>
<div className="faq-list">
{faq.map((item) => (
<details key={item.q} className="faq-item">
<summary>{item.q}</summary>
<p>{item.a}</p>
</details>
))}
</div>
</div>
</section>
</>
);
}

View File

@@ -1,20 +0,0 @@
import { Link } from 'react-router-dom';
export function NotFoundPage() {
return (
<div className="wrap page">
<article className="docs-content">
<h1>Page not found</h1>
<p className="intro">That address is not a LexAI page. Check the URL or go back home.</p>
<div className="cta-row">
<Link className="btn btn-primary" to="/">
Home
</Link>
<Link className="btn btn-ghost" to="/docs">
Docs
</Link>
</div>
</article>
</div>
);
}

View File

@@ -1,25 +0,0 @@
{
"origin": "https://lexai.juankibin.space",
"routes": [
{
"path": "/",
"title": "LexAI — Grammarly-style writing help with your own API key",
"description": "Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
},
{
"path": "/docs",
"title": "LexAI docs — Chrome, VS Code, CLI, and free LLM setup",
"description": "Install LexAI, add a free Groq or OpenRouter key, and use writing actions in Chrome or code assist in VS Code. Privacy and shared-core notes included."
},
{
"path": "/contribute",
"title": "Contribute to LexAI — open-source BYO-LLM assistant",
"description": "Report bugs, set up the LexAI monorepo (Chrome, VS Code, CLI, website), and send focused pull requests. MIT license, Code of Conduct."
},
{
"path": "/community",
"title": "LexAI community — source, issues, and installs",
"description": "Chrome Web Store, VS Code Marketplace, Gitea repository, issue tracker, and Code of Conduct for the LexAI project."
}
]
}

View File

@@ -1,86 +0,0 @@
import faq from './faq.json';
import routesFile from './seo.routes.json';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from './links';
export const SITE_ORIGIN = routesFile.origin;
export type SeoRoute = {
path: string;
title: string;
description: string;
index: boolean;
};
export const INDEXABLE_ROUTES: SeoRoute[] = routesFile.routes.map((r) => ({
...r,
index: true,
}));
export const NOT_FOUND_SEO: SeoRoute = {
path: '',
title: 'Page not found — LexAI',
description: 'That address is not a LexAI page.',
index: false,
};
const byPath = new Map(INDEXABLE_ROUTES.map((r) => [r.path, r]));
export function seoForPath(pathname: string): SeoRoute {
const normalized = pathname.replace(/\/+$/, '') || '/';
return byPath.get(normalized) ?? NOT_FOUND_SEO;
}
export function canonicalUrl(path: string): string {
if (!path || path === '/') return `${SITE_ORIGIN}/`;
return `${SITE_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`;
}
function softwareGraph() {
return {
'@type': 'SoftwareApplication',
name: 'LexAI',
applicationCategory: 'BrowserApplication',
applicationSubCategory: 'Writing assistance',
operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux',
url: `${SITE_ORIGIN}/`,
downloadUrl: [CHROME_STORE, VSCODE_MARKETPLACE],
license: `${REPO}/src/branch/main/LICENSE`,
sameAs: [REPO, CHROME_STORE, VSCODE_MARKETPLACE],
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
};
}
export function jsonLdForPath(pathname: string): Record<string, unknown> {
const seo = seoForPath(pathname);
const url = seo.index ? canonicalUrl(seo.path) : `${SITE_ORIGIN}${pathname}`;
const graph: unknown[] = [
{
'@type': 'WebSite',
name: 'LexAI',
url: `${SITE_ORIGIN}/`,
description: seoForPath('/').description,
},
softwareGraph(),
];
if (seo.path === '/') {
graph.push({
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
});
} else if (seo.index) {
graph.push({
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: `${SITE_ORIGIN}/` },
{ '@type': 'ListItem', position: 2, name: seo.title, item: url },
],
});
}
return { '@context': 'https://schema.org', '@graph': graph };
}

View File

@@ -1,746 +0,0 @@
:root {
--bg: #f7f7f5;
--surface: #ffffff;
--ink: #171717;
--muted: #5c5c57;
--line: #e6e6e2;
--indigo: #4f46e5;
--indigo-deep: #4338ca;
--bolt: #f5c14a;
--focus: #4f46e5;
--radius: 8px;
--font: 'Inter', 'Segoe UI', system-ui, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
color-scheme: light;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font);
font-size: 16px;
line-height: 1.6;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
a {
color: var(--ink);
text-underline-offset: 0.15em;
}
a:hover {
color: var(--muted);
}
img {
max-width: 100%;
display: block;
}
button {
font: inherit;
}
.shell {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.wrap {
width: min(920px, calc(100% - 2.5rem));
margin-inline: auto;
}
/* Header */
.header {
position: sticky;
top: 0;
z-index: 20;
background: rgba(247, 247, 245, 0.86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.85rem 0;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--ink);
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
letter-spacing: -0.02em;
}
.brand:hover {
color: var(--ink);
}
.brand svg {
display: block;
border-radius: 6px;
}
.nav {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem 1.15rem;
}
.nav a:not(.btn),
.nav-link-btn {
color: var(--muted);
text-decoration: none;
font-weight: 500;
font-size: 0.875rem;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.nav a:not(.btn):hover,
.nav-link-btn:hover,
.nav a[aria-current='page']:not(.btn) {
color: var(--ink);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0.5rem 0.95rem;
border-radius: 6px;
border: 1px solid transparent;
font-weight: 550;
font-size: 0.875rem;
text-decoration: none;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.btn:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.btn-primary {
background: var(--indigo);
color: #fff;
border-color: var(--indigo);
}
.btn-primary:hover {
background: var(--indigo-deep);
color: var(--bolt);
border-color: var(--indigo-deep);
}
.btn-ghost {
background: transparent;
border-color: var(--line);
color: var(--ink);
}
.btn-ghost:hover {
border-color: #cfcfc9;
color: var(--ink);
}
/* Hero */
.hero {
padding: clamp(4.5rem, 12vw, 7.5rem) 0 4rem;
}
.hero-lockup {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0 0 1.5rem;
}
.hero-lockup svg {
display: block;
border-radius: 14px;
}
.hero-wordmark {
font-weight: 600;
font-size: 1.35rem;
letter-spacing: -0.03em;
}
.hero-wordmark span {
color: var(--indigo);
}
.hero-kicker {
margin: 0 0 1rem;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.hero h1 {
margin: 0 0 1rem;
font-weight: 560;
font-size: clamp(2rem, 4.6vw, 2.75rem);
line-height: 1.15;
letter-spacing: -0.035em;
max-width: 18ch;
}
.hero-lead {
margin: 0 0 1.75rem;
max-width: 42ch;
color: var(--muted);
font-size: 1.05rem;
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
/* Sections */
.section {
padding: 3.5rem 0;
}
.section + .section {
border-top: 1px solid var(--line);
}
.section-kicker {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--indigo);
}
.section h2 {
margin: 0 0 0.5rem;
font-weight: 560;
font-size: 1.45rem;
letter-spacing: -0.025em;
}
.section-lead {
margin: 0 0 2rem;
max-width: 48ch;
color: var(--muted);
}
.faq-list {
display: grid;
gap: 0.5rem;
}
.faq-item {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
}
.faq-item summary {
cursor: pointer;
list-style: none;
min-height: 44px;
padding: 0.75rem 1rem;
font-weight: 560;
}
.faq-item summary::-webkit-details-marker {
display: none;
}
.faq-item p {
margin: 0;
padding: 0 1rem 1rem;
color: var(--muted);
}
.crumbs {
margin: 0 0 1rem;
font-size: 0.875rem;
}
.crumbs ol {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin: 0;
padding: 0;
list-style: none;
color: var(--muted);
}
.crumbs li:not(:last-child)::after {
content: '/';
margin-left: 0.35rem;
color: var(--line);
}
.crumbs a {
color: var(--muted);
}
.feature-list {
display: grid;
gap: 0;
list-style: none;
margin: 0;
padding: 0;
}
@media (min-width: 700px) {
.feature-list {
grid-template-columns: 1fr 1fr;
column-gap: 3rem;
}
}
.feature-list li {
padding: 1.15rem 0;
border-top: 1px solid var(--line);
}
.feature-list h3 {
margin: 0 0 0.3rem;
font-size: 0.98rem;
font-weight: 600;
}
.feature-list p {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.compare-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 700px) {
.compare-grid {
grid-template-columns: 1fr 1fr;
}
}
.package-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 760px) {
.package-grid {
grid-template-columns: repeat(3, 1fr);
}
}
a.package-card {
text-decoration: none;
}
.package-card {
display: flex;
flex-direction: column;
gap: 0.55rem;
padding: 1.2rem 1.15rem 1.25rem;
border-radius: var(--radius);
background: var(--surface);
border: 1px solid var(--line);
color: inherit;
min-height: 100%;
text-align: left;
cursor: pointer;
width: 100%;
}
.package-card:hover {
border-color: #c8c8c2;
}
.package-card strong {
font-size: 0.98rem;
font-weight: 600;
}
.package-card p {
margin: 0;
flex: 1;
color: var(--muted);
font-size: 0.9rem;
}
.package-card .more {
font-weight: 500;
font-size: 0.82rem;
color: var(--ink);
}
.privacy {
padding-top: 0.25rem;
}
.privacy h2 {
margin-top: 0;
}
.privacy ul {
margin: 0.75rem 0 0;
padding-left: 1.1rem;
color: var(--muted);
}
/* Free tier section */
.free-highlights {
display: grid;
gap: 0;
list-style: none;
margin: 0 0 2rem;
padding: 0;
}
@media (min-width: 700px) {
.free-highlights {
grid-template-columns: 1fr 1fr;
column-gap: 3rem;
}
}
.free-highlights li {
padding: 1rem 0;
border-top: 1px solid var(--line);
}
.free-highlights strong {
display: block;
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
.free-highlights p {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.provider-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 700px) {
.provider-grid {
grid-template-columns: 1fr 1fr;
}
}
.provider-card {
display: flex;
flex-direction: column;
gap: 0.55rem;
padding: 1.2rem 1.15rem 1.25rem;
border-radius: var(--radius);
background: var(--surface);
border: 1px solid var(--line);
}
.provider-card-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.provider-card strong {
font-size: 0.98rem;
font-weight: 600;
}
.provider-card > p {
margin: 0;
flex: 1;
color: var(--muted);
font-size: 0.9rem;
}
.provider-badge {
display: inline-block;
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.provider-badge-best {
background: #eef2ff;
color: var(--indigo-deep);
}
.provider-badge-no-card {
background: #ecfdf5;
color: #047857;
}
.provider-badge-trial {
background: #fef9ec;
color: #92400e;
}
.provider-meta {
display: grid;
gap: 0.45rem;
margin: 0;
font-size: 0.82rem;
}
.provider-meta dt {
margin: 0;
font-weight: 600;
color: var(--ink);
}
.provider-meta dd {
margin: 0.1rem 0 0;
color: var(--muted);
}
.provider-card .more {
font-weight: 500;
font-size: 0.82rem;
color: var(--indigo);
text-decoration: none;
}
.provider-card .more:hover {
color: var(--indigo-deep);
}
.free-footnote {
margin: 1.25rem 0 0;
font-size: 0.88rem;
color: var(--muted);
}
.docs-provider {
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid var(--line);
}
.docs-provider:first-of-type {
margin-top: 0.75rem;
}
.docs-provider h4 {
margin: 0 0 0.35rem;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--ink);
text-transform: none;
}
.docs-provider > p {
margin: 0 0 0.5rem;
}
/* Inner pages (Contribute, Community) */
.page {
padding: 2.75rem 0 4.5rem;
}
.docs-block ol {
margin: 0.45rem 0 0;
padding-left: 1.15rem;
color: var(--muted);
}
.docs-block ol li + li {
margin-top: 0.22rem;
}
/* Docs */
.docs-layout {
display: grid;
gap: 2.25rem;
padding: 2.75rem 0 4.5rem;
}
@media (min-width: 860px) {
.docs-layout {
grid-template-columns: 160px 1fr;
gap: 3rem;
align-items: start;
}
}
.docs-nav {
position: sticky;
top: 4.25rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.docs-nav .nav-link-btn {
color: var(--muted);
font-weight: 500;
font-size: 0.875rem;
padding: 0.3rem 0;
text-align: left;
background: none;
border: none;
cursor: pointer;
}
.docs-nav .nav-link-btn:hover {
color: var(--ink);
}
.docs-content h1 {
margin: 0 0 0.65rem;
font-weight: 560;
font-size: clamp(1.75rem, 3.5vw, 2.15rem);
letter-spacing: -0.03em;
}
.docs-content .intro {
margin: 0 0 1.75rem;
color: var(--muted);
max-width: 58ch;
}
.docs-block {
scroll-margin-top: 5rem;
padding: 1.6rem 0;
border-top: 1px solid var(--line);
}
.docs-block h2 {
margin: 0 0 0.45rem;
font-weight: 560;
font-size: 1.25rem;
letter-spacing: -0.02em;
}
.docs-block h3 {
margin: 1.15rem 0 0.35rem;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.docs-block > p {
color: var(--muted);
margin: 0;
}
.docs-block ul {
margin: 0.45rem 0 0;
padding-left: 1.15rem;
color: var(--muted);
}
.docs-block li + li {
margin-top: 0.22rem;
}
.docs-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
code,
.mono {
font-family: var(--mono);
font-size: 0.86em;
color: var(--ink);
background: #eeebe6;
padding: 0.08em 0.32em;
border-radius: 4px;
}
pre.mono {
display: block;
padding: 0.95rem 1rem;
overflow-x: auto;
border-radius: 6px;
border: 1px solid var(--line);
background: var(--surface);
line-height: 1.5;
}
/* Footer */
.footer {
border-top: 1px solid var(--line);
padding: 1.35rem 0 1.75rem;
color: var(--muted);
font-size: 0.82rem;
}
.footer-inner {
display: flex;
flex-wrap: wrap;
gap: 0.65rem 1.25rem;
justify-content: space-between;
align-items: center;
}
.footer a {
color: var(--muted);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
color: var(--ink);
}
@media (max-width: 640px) {
.nav .hide-sm {
display: none;
}
}

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noEmitOnError": true
},
"include": ["src"]
}

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,6 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});