Compare commits
1 Commits
dev
...
df0bcae250
| Author | SHA1 | Date | |
|---|---|---|---|
| df0bcae250 |
@@ -4,7 +4,7 @@ Two hooks, both small, both readable in a minute, both safe to delete. They exis
|
||||
|
||||
| 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. |
|
||||
|
||||
## Why these two
|
||||
|
||||
@@ -132,10 +132,6 @@ function main(payload) {
|
||||
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
|
||||
? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
|
||||
: {};
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
100
.gitea/workflows/ci.yml
Normal 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"
|
||||
132
.gitea/workflows/deploy-chrome.yml
Normal file
132
.gitea/workflows/deploy-chrome.yml
Normal 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"
|
||||
53
.gitea/workflows/preview.yml
Normal file
53
.gitea/workflows/preview.yml
Normal 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 }}\`\"}"
|
||||
102
.gitea/workflows/release.yml
Normal file
102
.gitea/workflows/release.yml
Normal 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
3
.gitignore
vendored
@@ -11,6 +11,3 @@ dist/
|
||||
# Playwright
|
||||
test-results/
|
||||
.tmp-*
|
||||
|
||||
# Graphify knowledge graph (local, regenerable)
|
||||
graphify-out/
|
||||
|
||||
@@ -199,8 +199,7 @@ When the user corrects you, a test or review proves a mistake, or you discover a
|
||||
|
||||
## Lessons
|
||||
|
||||
- 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 .`.
|
||||
- *(Add one-line prevention rules here as mistakes are verified.)*
|
||||
|
||||
## Memory protocol
|
||||
|
||||
@@ -209,8 +208,8 @@ Subagents have no memory and cannot see this conversation — durable knowledge
|
||||
## 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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. `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`.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
- Style inline; Tailwind classes do nothing until PostCSS is wired.
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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. `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`.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -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`).
|
||||
- **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
|
||||
|
||||
1. Fork / branch from `main` (or `develop` if that is the active integration branch).
|
||||
|
||||
18
README.md
18
README.md
@@ -5,9 +5,8 @@
|
||||
| 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/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/website](packages/website)** | Marketing site + docs (features & all packages) | [README](packages/website/README.md) |
|
||||
|
||||
Shared provider/prompt core: [`src/lib`](src/lib).
|
||||
|
||||
@@ -26,17 +25,12 @@ npm run chrome:dev
|
||||
|
||||
# VS Code extension
|
||||
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
|
||||
npm run cli:build
|
||||
export LEXAI_API_KEY=sk-... # required; PowerShell: $env:LEXAI_API_KEY="sk-..."
|
||||
export LEXAI_PROVIDER=openai # optional: openai | anthropic | groq | openrouter
|
||||
export LEXAI_MODEL=gpt-4o # optional
|
||||
export LEXAI_API_KEY=sk-... # PowerShell: $env:LEXAI_API_KEY="sk-..."
|
||||
node packages/cli/out/cli.js prompt "add rate limiting to the auth routes"
|
||||
|
||||
# Website (features + docs)
|
||||
npm run website:dev
|
||||
```
|
||||
|
||||
## Repository layout
|
||||
@@ -46,8 +40,7 @@ LexAI/
|
||||
├── packages/
|
||||
│ ├── chrome/ # Manifest V3 extension (WXT + React)
|
||||
│ ├── vscode/ # VS Code / Cursor extension
|
||||
│ ├── cli/ # lexai prompt CLI
|
||||
│ └── website/ # Marketing site + docs
|
||||
│ └── cli/ # lexai prompt CLI
|
||||
├── src/lib/ # Shared providers, actions, types
|
||||
├── tests/ # Unit + e2e tests (run via chrome package)
|
||||
├── docs/ # Project operating docs (agents / planning)
|
||||
@@ -63,7 +56,7 @@ OpenAI · Anthropic · Groq · OpenRouter
|
||||
- No LexAI servers and no LexAI telemetry
|
||||
- Chrome: key encrypted in `chrome.storage.local`
|
||||
- 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
|
||||
|
||||
@@ -76,5 +69,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT
|
||||
## Links
|
||||
|
||||
- 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
|
||||
|
||||
@@ -22,26 +22,6 @@ Add an entry when a choice is hard to reverse, shapes future work, or a future m
|
||||
|
||||
## 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
|
||||
|
||||
- **Status:** accepted
|
||||
|
||||
@@ -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 |
|
||||
@@ -4,15 +4,7 @@ This file contains small, repeatable checks derived from verified failures. Pref
|
||||
|
||||
## Active failure-derived checks
|
||||
|
||||
### E-2026-08-17-01 — Website production server answers GET /
|
||||
|
||||
- **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)
|
||||
_No failure-derived checks yet._
|
||||
|
||||
## Eval template
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Current state
|
||||
|
||||
- **Outcome:** Standing workflow: commit after each landed task; session end `graphify update .`.
|
||||
- **Shipped:** `AGENTS.md` / `CLAUDE.md` lessons + state continuity; `.cursor/rules/graphify.mdc`; session-start reminder; `D-2026-09-04-01`.
|
||||
- **Verified:** docs/rules updated; graphify-out gitignored.
|
||||
- **Next smallest action:** Continue product work; this session will commit this change and run `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.
|
||||
- **Verified:** run `npm run chrome:install` + typecheck/test/build after pull (session in progress).
|
||||
- **CI:** workflows use `packages/chrome` for install/build/zip paths.
|
||||
- **Next smallest action:** `npm run chrome:install && npm run chrome:typecheck && npm run chrome:test && npm run chrome:build` then commit when ready.
|
||||
|
||||
@@ -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.
|
||||
|
||||
### L-2026-09-04-01 — Commit each landed task; session end `graphify update .`
|
||||
|
||||
- **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
|
||||
_No active guardrails yet._
|
||||
|
||||
## Recording policy
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
## 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`.
|
||||
- Providers: OpenAI, Anthropic, Groq, OpenRouter — `callProvider` / `PROVIDER_SPECS`.
|
||||
- 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/`.
|
||||
- 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).
|
||||
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
|
||||
- VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`.
|
||||
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`, `website:*`.
|
||||
- After each landed task: git commit (no secrets; `graphify-out/` ignored). Session end: `graphify update .`. Do not push unless asked.
|
||||
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`.
|
||||
|
||||
## Environment quirks
|
||||
|
||||
@@ -33,10 +32,8 @@
|
||||
| Shared lib | `src/lib/` |
|
||||
| VS Code | `packages/vscode/` |
|
||||
| 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` |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -2,28 +2,29 @@
|
||||
|
||||
> 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
|
||||
|
||||
- Chrome extension: select on any page → AI action → Replace/Copy
|
||||
- VS Code extension: 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)
|
||||
- Chrome extension (existing): select on any page → AI action → Replace/Copy
|
||||
- VS Code extension (new): select in editor → LexAI context menu / Command Palette → selection replaced
|
||||
- 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`.
|
||||
2. Home should end with an FAQ. Docs should show Home / Docs crumbs.
|
||||
3. Search Console → URL inspection → request indexing for `https://lexai.juankibin.space/` and `/docs`.
|
||||
1. From repo root: `npm run vscode:install` then `npm run vscode:build`
|
||||
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. 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
|
||||
|
||||
| # | 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 |
|
||||
| 2 | Request indexing for Home and Docs | **Do after deploy** / later | Faster first index |
|
||||
| 3 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet |
|
||||
| 1 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch for builders/critics |
|
||||
| 2 | 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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -8,7 +8,6 @@ _No active task contracts._
|
||||
|
||||
## 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).
|
||||
|
||||
## Task contract format
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
12
package.json
12
package.json
@@ -28,14 +28,8 @@
|
||||
"cli:install": "npm install --prefix packages/cli",
|
||||
"cli:build": "npm run compile --prefix packages/cli",
|
||||
"cli:typecheck": "npm run typecheck --prefix packages/cli",
|
||||
"website:install": "npm install --prefix packages/website",
|
||||
"website:dev": "npm run dev --prefix packages/website",
|
||||
"website:build": "npm run build --prefix packages/website",
|
||||
"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"
|
||||
"install:all": "npm run chrome:install && npm run vscode:install && npm run cli:install",
|
||||
"typecheck:all": "npm run chrome:typecheck && npm run vscode:typecheck && npm run cli:typecheck",
|
||||
"build:all": "npm run chrome:build && npm run vscode:build && npm run cli:build"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
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({
|
||||
root: repoRoot,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@lib': libRoot,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: [repoRoot],
|
||||
'@lib': fileURLToPath(new URL('../../src/lib', import.meta.url)),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: [setupFile],
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
exclude: ['tests/e2e/**/*', 'packages/**'],
|
||||
setupFiles: ['../../tests/unit/setup.ts'],
|
||||
include: ['../../tests/unit/**/*.test.ts'],
|
||||
exclude: ['../../tests/e2e/**/*'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,15 +10,7 @@
|
||||
|
||||
---
|
||||
|
||||
## A. Install from the Marketplace (recommended)
|
||||
|
||||
[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)
|
||||
## A. Install on your machine (sideload)
|
||||
|
||||
### VS Code
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ Change in Settings or via option chips in the suggestion zone.
|
||||
|
||||
## 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.
|
||||
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).
|
||||
@@ -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) |
|
||||
| **Source** | [LexAI repository](https://git.juankibin.space/kibin/LexAI) |
|
||||
| **Publisher** | JuanKibin |
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
*.md
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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(', '));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
1951
packages/website/package-lock.json
generated
1951
packages/website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -1,4 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://lexai.juankibin.space/sitemap.xml
|
||||
@@ -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>
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
@@ -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;
|
||||
@@ -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`;
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
1
packages/website/src/vite-env.d.ts
vendored
1
packages/website/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Reference in New Issue
Block a user