feat(cli): add initial CLI implementation with argument parsing and prompt handling
Some checks failed
CI — Test & Build / Test & Build (pull_request) Has been cancelled
Preview — PR Build Check / PR Preview Build (pull_request) Has been cancelled

- Created package.json and package-lock.json for CLI package.
- Implemented argument parsing in args.ts to handle various flags and commands.
- Developed main CLI logic in cli.ts to execute commands and handle errors.
- Added configuration loading from a JSON file in config.ts, with environment variable support.
- Implemented prompt resolution and provider interaction in prompt.ts.
- Added usage documentation for the CLI.
- Configured TypeScript settings in tsconfig.json for the CLI package.
- Updated README in vscode package to reflect the new CLI functionality.
- Refactored root tsconfig.json to streamline project structure.
This commit is contained in:
john kevin asprec
2026-08-13 19:19:42 +08:00
parent d63d698b57
commit 2c00d2e431
37 changed files with 9303 additions and 166 deletions

View File

@@ -0,0 +1,48 @@
---
name: Bug report
about: Report a problem with LexAI (Chrome, VS Code, or CLI)
title: "[bug] "
labels: bug
---
## Summary
A clear, one-sentence description of the bug.
## Package
Which LexAI surface is affected?
- [ ] Chrome extension (`packages/chrome`)
- [ ] VS Code / Cursor extension (`packages/vscode`)
- [ ] CLI Prompt Builder (`packages/cli`)
- [ ] Shared library (`src/lib`) / unclear
## Environment
- LexAI version / commit (if known):
- OS:
- Browser / VS Code / Cursor version (if relevant):
- Provider + model (no API keys):
## Steps to reproduce
1.
2.
3.
## Expected behavior
What you expected to happen.
## Actual behavior
What happened instead. Include error messages (redact secrets).
## Screenshots / logs
Attach if helpful. **Never paste API keys or encrypted key material.**
## Additional context
Anything else that might help (page type, selection size, offline, etc.).

View File

@@ -0,0 +1,24 @@
## Summary
Briefly describe what this PR changes and why.
## Package(s)
- [ ] `packages/chrome`
- [ ] `packages/vscode`
- [ ] `packages/cli`
- [ ] `src/lib` (shared)
- [ ] Docs / CI / repo meta
## Test plan
- [ ] `npm run chrome:typecheck` / `npm run chrome:test` (if Chrome or shared lib)
- [ ] `npm run vscode:typecheck` / `npm run vscode:build` (if VS Code)
- [ ] `npm run cli:typecheck` / `npm run cli:build` (if CLI)
- [ ] Manual check (describe):
## Checklist
- [ ] No API keys, tokens, or personal data in the diff
- [ ] Docs / README updated if user-facing behavior changed
- [ ] Related issue linked (if any): #

View File

@@ -29,32 +29,30 @@ jobs:
git checkout ${{ gitea.sha }}
fi
- name: Install dependencies
- name: Install Chrome package dependencies
run: npm ci --prefer-offline --no-audit --no-fund
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
# Generates .wxt/types (ImportMeta.env etc.) required by typecheck.
# Also runs via the postinstall hook — kept explicit so a future
# --ignore-scripts install can't silently break the typecheck step.
- name: Prepare WXT types
run: npx wxt prepare
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
- name: Type check
run: npm run typecheck
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
- name: Run unit tests
run: npm test -- --run
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
- name: Build extension
run: npm run build
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
- name: Verify build output
run: ls -la .output/chrome-mv3/
working-directory: /tmp/lexai
working-directory: /tmp/lexai/packages/chrome
- name: Package as ZIP
run: |
@@ -64,7 +62,7 @@ jobs:
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
working-directory: /tmp/lexai/packages/chrome
- name: Publish to Gitea Package Registry
if: gitea.event_name == 'push'

View File

@@ -30,24 +30,29 @@ jobs:
- 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="$(pwd)/lexai-chrome-mv3-${VERSION}.zip"
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
@@ -55,6 +60,7 @@ jobs:
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

View File

@@ -23,21 +23,26 @@ jobs:
- 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/
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: |

View File

@@ -23,20 +23,24 @@ jobs:
- 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="$(pwd)/lexai-chrome-mv3-${VERSION}.zip"
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: |

3
.gitignore vendored
View File

@@ -1,9 +1,12 @@
node_modules/
.output/
.wxt/
packages/*/.output/
packages/*/.wxt/
dist/
*.local
*.zip
*.vsix
# Playwright
test-results/

View File

@@ -10,16 +10,16 @@
| Field | Value |
| --- | --- |
| Project | LexAI — BYO-LLM writing help (Chrome MV3 + VS Code) |
| Outcome | Select text → AI action (fix/rephrase/shorten/expand/explain/prompt) → replace in place (Chrome also Copy), with no backend and no subscription |
| Non-goals | no backend/account/subscription; no telemetry; no transmission of text/key except to the user's chosen provider; not a full editor; VS Code v1 skips floating toolbar / Prompt Builder / Copy As |
| Primary user | people who hold an LLM API key and want inline writing help without a SaaS subscription |
| Acceptance tests | Chrome: `typecheck` + `test -- --run` + loadable `.output/chrome-mv3/` + encrypted key path; VS Code: `vscode:typecheck` + `vscode:build` + Secret Storage key + selection replace |
| Constraints | WXT ^0.20 + React 18 + TS (Chrome); `packages/vscode` + esbuild; shared `src/lib`; Node 22; Gitea CI + Chrome Web Store |
| Source of truth | this file + `CLAUDE.md` + `docs/`; Plane (LEXAI); `docs/TASKS.md` |
| Reference bar | not yet concrete — proposal in `docs/REFERENCE_BAR.md`; no gauntlet until artifacts exist |
| Model routing | `docs/MODEL_ROUTING.md` — filled 2026-08-13 |
| Commands | `install: npm install` · `test: npm test -- --run` · `lint: npm run typecheck` · `build: npm run build` · `vscode:build` / `vscode:typecheck` / `vscode:package` |
| Project | LexAI — BYO-LLM writing help (Chrome MV3 + VS Code + CLI) |
| Outcome | Select text/code → AI help without LexAI backend or subscription |
| Non-goals | no backend/account/subscription; no telemetry; key/text only to user's provider; not a full editor |
| Primary user | people who hold an LLM API key and want inline writing / prompt help |
| Acceptance tests | `chrome:typecheck` + `chrome:test` + loadable `packages/chrome/.output/chrome-mv3/`; `vscode:typecheck` + `vscode:build`; `cli:typecheck` + `cli:build` |
| Constraints | Monorepo `packages/{chrome,vscode,cli}` + `src/lib`; Node 22; Gitea CI + Chrome Web Store |
| Source of truth | this file + `CLAUDE.md` + `docs/` + root `README.md`; Plane (LEXAI) |
| Reference bar | proposal in `docs/REFERENCE_BAR.md`; no gauntlet until artifacts exist |
| Model routing | `docs/MODEL_ROUTING.md` |
| Commands | `install:all` · `chrome:*` · `vscode:*` · `cli:*` |
### Definition of done

View File

@@ -12,15 +12,15 @@
| Field | Value |
| --- | --- |
| Project | LexAI — Grammarly-like Chrome extension (Manifest V3), BYO-LLM-key |
| Outcome | Select text on any page → AI action (fix/rephrase/shorten/expand/explain/prompt) → Replace or Copy, with no backend and no subscription |
| Project | LexAI — BYO-LLM Chrome MV3 + VS Code + CLI Prompt Builder |
| Outcome | Select text/code → AI help (writing actions / Code Assist / prompt engineer) with no LexAI backend or subscription |
| Non-goals | no backend/account/subscription; no telemetry; no transmission of text/key except to the user's chosen provider; not a full editor |
| Primary user | people who hold an LLM API key and want inline writing help without a SaaS subscription |
| Acceptance tests | `npm run typecheck` + `npm test -- --run` pass; `npm run build` yields a loadable `.output/chrome-mv3/`; Replace works on textarea/input and contenteditable; key uses the encrypted path and is never logged/exfiltrated |
| Constraints | WXT ^0.20 + React 18 + TS; Node 22; Tailwind inactive (inline styles); `<all_urls>` today; Gitea CI + Chrome Web Store |
| Source of truth | this file + `docs/`; issue tracking in Plane (LEXAI); task list `docs/TASKS.md` (derived from `RECOMMENDATIONS.md`) |
| Reference bar | not yet supplied — decision-ready proposals in `docs/REFERENCE_BAR.md` (candidate: Grammarly's selection-toolbar/card UX captured as screenshots into `docs/reference/`); a gauntlet does not start until the bar is concrete |
| Commands | `install: npm install` · `test: npm test -- --run` · `lint: npm run typecheck` · `build: npm run build` · `zip: npm run zip` · e2e: `npm run test:e2e` (after build) |
| Primary user | people who hold an LLM API key and want inline writing / prompt help without a SaaS subscription |
| Acceptance tests | `npm run chrome:typecheck` + `chrome:test`; Chrome build → `packages/chrome/.output/chrome-mv3/`; `vscode:typecheck` + `vscode:build`; `cli:typecheck` + `cli:build`; key never logged/exfiltrated |
| Constraints | Monorepo `packages/{chrome,vscode,cli}` + `src/lib`; WXT ^0.20 + React 18; Node 22; Gitea CI + Chrome Web Store |
| Source of truth | this file + `docs/` + root `README.md`; Plane (LEXAI); `docs/TASKS.md` |
| Reference bar | decision-ready proposals in `docs/REFERENCE_BAR.md` |
| Commands | `install:all` · `chrome:dev|build|test|typecheck|zip` · `vscode:*` · `cli:*` |
### Definition of done
@@ -32,57 +32,36 @@ Work is done only when the requested outcome is implemented, relevant checks pas
A Grammarly-like **Chrome Extension (Manifest V3)** providing AI writing assistance (grammar fix, rephrase, shorten, expand, explain) on any webpage. Users bring **their own LLM API key** — there is no LexAI backend. The service worker calls the user's chosen provider directly.
- **Providers:** OpenAI, Anthropic, Groq, OpenRouter (configured in `entrypoints/background.ts`).
- **No subscription, no server.** The API key lives encrypted in `chrome.storage.local`.
- **Providers:** OpenAI, Anthropic, Groq, OpenRouter (`packages/chrome/entrypoints/background.ts`).
- **Also:** VS Code twin (`packages/vscode`), CLI Prompt Builder (`packages/cli`), shared `src/lib`.
- **No subscription, no server.** Chrome key encrypted in `chrome.storage.local`.
#### Tech stack
- **WXT** `^0.20`extension framework (wraps Vite). Entrypoints in `entrypoints/`.
- **WXT** `^0.20`Chrome package under `packages/chrome/` (entrypoints there).
- **React 18** + TypeScript — Options and Popup pages only.
- **Zustand** — a dependency, but state is currently local; not yet wired into a store.
- **tweetnacl** / **tweetnacl-util**`secretbox` symmetric encryption for the API key.
- **Vitest** (jsdom) unit tests, **Playwright** e2e.
- **Tailwind** is in devDependencies but **not active** — all UI uses inline style objects (WXT PostCSS was never wired up). Do not assume Tailwind classes work.
- **tweetnacl** — `secretbox` for the Chrome API key.
- **Vitest** / **Playwright** — configured in `packages/chrome`.
- **Tailwind** inactive — inline styles only.
#### Commands
```bash
npm install # first-time setup (node_modules gitignored; not present by default)
npm run dev # WXT dev server with hot reload
npm run build # production build → .output/chrome-mv3/
npm run zip # package for Chrome Web Store
npm test # vitest (add `-- --run` for one-shot, non-watch)
npm run test:e2e # Playwright (requires a prior `npm run build`)
npm run typecheck # tsc --noEmit
npm run install:all
npm run chrome:dev
npm run chrome:build # → packages/chrome/.output/chrome-mv3/
npm run chrome:zip
npm run chrome:test
npm run chrome:typecheck
npm run vscode:build
npm run cli:build
```
**Prerequisite:** Node. CI pins **Node 22** (`node:22-bookworm`). Run `npm install` before any `npm run *` — the binaries (`tsc`, `vitest`) come from `node_modules/.bin`. Install triggers `postinstall: wxt prepare`, which generates `.wxt/types/`**typecheck fails without it** (`import.meta.env` is typed there). If a fresh clone errors with `Property 'env' does not exist on type 'ImportMeta'`, run `npx wxt prepare`.
**Load unpacked in Chrome:** `npm run build``chrome://extensions` → Developer Mode → Load unpacked → select `.output/chrome-mv3`.
**Prerequisite:** Node **22**. Chrome `postinstall` runs `wxt prepare`. Load unpacked from `packages/chrome/.output/chrome-mv3`.
#### Architecture
Three cooperating contexts, message-passed over `chrome.runtime`:
```
entrypoints/content.ts (content script, injected into <all_urls>)
• Detects text selection: textarea/input (selectionStart/End) vs contenteditable/DOM (Range API)
• Renders the floating toolbar + result modal + toasts (inline-styled, appended to document.body)
• Snapshots selection state BEFORE any async call, then Replace uses the snapshot
• Sends { type: 'ANALYZE_TEXT', payload: {text, action, style} } to the background
entrypoints/background.ts (service worker — the LLM proxy)
• onMessage: ANALYZE_TEXT and COPY_AS
• Reads provider/apiKey/apiKeyEnc/encKey/model from chrome.storage.local
• Decrypts the key (tweetnacl secretbox), routes to the correct provider's fetch call
• Registers right-click context menus (action × style) on install
entrypoints/options/Options.tsx (settings page, React)
• Provider + model + API key form; encrypts the key and writes apiKeyEnc/encKey to storage
entrypoints/popup/Popup.tsx (toolbar popup, React)
• Standalone text box → same ANALYZE_TEXT flow; shows config status; links to Options
```
Chrome: three contexts over `chrome.runtime` under `packages/chrome/entrypoints/` (`content`, `background`, `options`, `popup`). VS Code and CLI reuse `@lib` from `src/lib`.
Content script and popup **must not** call provider APIs directly — CORS and key handling belong in the background service worker. Route everything through `ANALYZE_TEXT`/`COPY_AS`. See `docs/ARCHITECTURE.md` for the component table.

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at the contact
listed on the project repository or Chrome Web Store developer listing.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

62
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,62 @@
# Contributing to LexAI
Thanks for helping improve LexAI. This project is a monorepo with three user-facing packages plus a shared library.
## Code of Conduct
Please read and follow the [Code of Conduct](CODE_OF_CONDUCT.md).
## Ways to contribute
- Bug reports and reproducible issues
- Documentation fixes
- Small, focused pull requests (one concern per PR)
- Discussions on design when a change is large or security-sensitive
## Development setup
**Requirements:** Node.js **22+**, npm 11+ (see `packageManager` in root `package.json`).
```bash
git clone https://git.juankibin.space/kibin/LexAI.git
cd LexAI
npm run install:all
```
### Useful scripts (from repo root)
| Script | Purpose |
| --- | --- |
| `npm run chrome:dev` | Chrome extension with hot reload |
| `npm run chrome:build` | Production Chrome build |
| `npm run chrome:typecheck` / `chrome:test` | Typecheck & unit tests |
| `npm run vscode:build` / `vscode:typecheck` | VS Code extension |
| `npm run cli:build` / `cli:typecheck` | CLI Prompt Builder |
| `npm run typecheck:all` / `build:all` | All packages |
Shared code lives in `src/lib/`. **Do not** import `@lib/crypto` or `@lib/messaging` from `packages/vscode` or `packages/cli` (Chrome-only).
### Package-specific notes
- **Chrome:** load unpacked from `packages/chrome/.output/chrome-mv3` after build. Selection/replace is DOM-timing-sensitive — verify on a real page for UI changes.
- **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`.
## Pull request process
1. Fork / branch from `main` (or `develop` if that is the active integration branch).
2. Keep changes scoped; update docs when behavior changes.
3. Run the checks that touch your change, at minimum:
- Shared lib / Chrome: `npm run chrome:typecheck` and `npm run chrome:test`
- VS Code: `npm run vscode:typecheck` and `npm run vscode:build`
- CLI: `npm run cli:typecheck` and `npm run cli:build`
4. Open a PR using the [pull request template](.gitea/PULL_REQUEST_TEMPLATE.md).
5. Do not include secrets, API keys, or personal data in commits or screenshots.
## Security
If you discover a vulnerability (especially around API key handling or data exfiltration), **do not** open a public issue with exploit details. Contact the maintainer privately (see the Chrome Web Store listing / repository owner).
## License
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 LexAI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,41 +1,72 @@
# LexAI — AI Writing Assistant
# LexAI
A Grammarly-like Chrome Extension powered by **your own LLM API key**. No monthly subscription. Full control.
**Bring your own LLM key.** LexAI is an open-source writing (and coding) assistant with **no LexAI backend, no account, and no subscription**. Your text and API key go only to the provider you choose.
## Features (Roadmap)
- ✅ Grammar & spelling correction
- ✅ Rephrase / rewrite text
- ✅ Shorten or expand text
- 🔜 Tone suggestions
- 🔜 Autocomplete
- 🔜 Custom style profiles
| 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) · [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) |
## Supported LLM Providers
- **OpenAI** (GPT-4o, GPT-4o-mini)
- **Anthropic** (Claude 3.5 Haiku, Sonnet)
- **Groq** (free tier — fast!)
- **OpenRouter** (100+ models)
Shared provider/prompt core: [`src/lib`](src/lib).
## Tech Stack
- WXT + React 18 + TypeScript
- Manifest V3
- Inline-styled UI (no CSS framework)
## Development
## Quick start
```bash
npm install
npm run dev # Dev mode with hot reload
npm run build # Production build
npm run zip # Package for Chrome Web Store
npm test # Run unit tests
# Prerequisites: Node 22+
git clone https://git.juankibin.space/kibin/LexAI.git
cd LexAI
# Install all packages
npm run install:all
# Chrome extension (dev)
npm run chrome:dev
# VS Code extension
npm run vscode:build
# then F5 / Install from VSIX — see packages/vscode/README.md
# CLI Prompt Builder
npm run cli:build
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"
```
## Load in Chrome
1. Run `npm run build`
2. Open Chrome → `chrome://extensions`
3. Enable **Developer Mode**
4. Click **Load unpacked** → select `.output/chrome-mv3`
## Repository layout
## Project Management
Plane: LEXAI project → https://plane-pro.juankibin.space
```text
LexAI/
├── packages/
│ ├── chrome/ # Manifest V3 extension (WXT + React)
│ ├── vscode/ # VS Code / Cursor extension
│ └── cli/ # lexai prompt CLI
├── src/lib/ # Shared providers, actions, types
├── tests/ # Unit + e2e tests (run via chrome package)
├── docs/ # Project operating docs (agents / planning)
└── .gitea/ # CI workflows + issue/PR templates
```
## Supported providers
OpenAI · Anthropic · Groq · OpenRouter
## Privacy
- 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` environment variable only
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT.md).
## License
[MIT](LICENSE) © LexAI contributors
## Links
- Chrome Web Store: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
- Source: https://git.juankibin.space/kibin/LexAI

View File

@@ -4,7 +4,7 @@
## Current state
- **Outcome:** VSIX install built; deploy guide written.
- **Artifact:** `packages/vscode/lexai-vscode-0.1.0.vsix` (~44 KB). Guide: `packages/vscode/DEPLOY.md`.
- **Verified:** `npm run package` in `packages/vscode` succeeded.
- **Next smallest action:** Install from VSIX locally; for public store, create Marketplace publisher + `vsce publish` (set `private: false` first).
- **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.

View File

@@ -4,19 +4,20 @@
## Verified facts
- LexAI has two clients: Chrome MV3 (repo root / WXT) and VS Code (`packages/vscode`).
- Portable core: `src/lib/providers.ts`, `actions.ts`, `types.ts`. Chrome-only: `crypto.ts`, `messaging.ts`.
- Providers: OpenAI, Anthropic, Groq, OpenRouter — routed by `callProvider` / `PROVIDER_SPECS`.
- VS Code v1: Command Palette + editor context submenu; API key in Secret Storage; prefs `lexai.provider|model|writingStyle`.
- Selection minimum length for actions: `MIN_SELECTION_LENGTH` (10) in `src/lib/actions.ts`.
- Chrome message listener must `return true` for async replies; snapshot selection before `await`.
- 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`; 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`.
## Conventions
- Style Chrome UI with inline styles (Tailwind not wired).
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
- VS Code must not import `@lib/crypto` or `@lib/messaging`.
- Root scripts: `vscode:install`, `vscode:build`, `vscode:typecheck`, `vscode:package`.
- VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`.
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`.
## Environment quirks
@@ -27,11 +28,12 @@
| What | Where |
| --- | --- |
| Chrome content / background / options / popup | `entrypoints/` |
| Chrome entrypoints | `packages/chrome/entrypoints/` |
| Shared lib | `src/lib/` |
| VS Code extension | `packages/vscode/src/extension.ts` |
| VS Code bundle | `packages/vscode/out/extension.js` |
| Model lanes | `docs/MODEL_ROUTING.md` |
| VS Code | `packages/vscode/` |
| CLI | `packages/cli/` |
| OSS meta | root `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` |
| Issue/PR templates | `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md` |
## Expiring notes

View File

@@ -1,38 +1,35 @@
{
"name": "lexai",
"version": "1.1.0",
"description": "A Grammarly-like Chrome Extension powered by your own LLM provider and API key",
"private": true,
"description": "LexAI monorepo — BYO-LLM writing assistant for Chrome, VS Code, and CLI Prompt Builder",
"license": "MIT",
"engines": {
"node": ">=22"
},
"packageManager": "npm@11.13.0",
"scripts": {
"dev": "wxt",
"build": "wxt build",
"zip": "wxt zip",
"test": "vitest",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
"postinstall": "wxt prepare",
"chrome:install": "npm install --prefix packages/chrome",
"chrome:dev": "npm run dev --prefix packages/chrome",
"chrome:build": "npm run build --prefix packages/chrome",
"chrome:zip": "npm run zip --prefix packages/chrome",
"chrome:typecheck": "npm run typecheck --prefix packages/chrome",
"chrome:test": "npm test --prefix packages/chrome -- --run",
"dev": "npm run chrome:dev",
"build": "npm run chrome:build",
"zip": "npm run chrome:zip",
"typecheck": "npm run chrome:typecheck",
"test": "npm run chrome:test",
"test:e2e": "npm run test:e2e --prefix packages/chrome",
"vscode:install": "npm install --prefix packages/vscode",
"vscode:build": "npm run compile --prefix packages/vscode",
"vscode:typecheck": "npm run typecheck --prefix packages/vscode",
"vscode:package": "npm run package --prefix packages/vscode"
},
"dependencies": {
"@wxt-dev/module-react": "^1.1.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tweetnacl": "^1.0.3",
"wxt": "^0.20.18"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@types/chrome": "^0.1.37",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"jsdom": "^28.1.0",
"typescript": "^5.7.3",
"vitest": "^3.0.7"
"vscode:package": "npm run package --prefix packages/vscode",
"cli:install": "npm install --prefix packages/cli",
"cli:build": "npm run compile --prefix packages/cli",
"cli:typecheck": "npm run typecheck --prefix packages/cli",
"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"
}
}

49
packages/chrome/README.md Normal file
View File

@@ -0,0 +1,49 @@
# LexAI for Chrome
Grammarly-like **Manifest V3** writing assistant powered by **your own LLM API key**. No LexAI account, no subscription, no LexAI servers.
**Chrome Web Store:** [LexAI — AI Writing Assistant](https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk)
## Features
- Select text on any page → **Fix / Rephrase / Shorten / Expand / Explain / Make Prompt**
- Writing styles: Formal, Casual, Academic, Creative, Concise
- Floating toolbar + right-click context menu + popup editor
- Prompt Builder (patterns, persona, format)
- Providers: OpenAI · Anthropic · Groq · OpenRouter
- API key encrypted locally (`tweetnacl` secretbox)
## Develop
From the **repo root**:
```bash
npm run chrome:install
npm run chrome:dev # hot reload
npm run chrome:build # → packages/chrome/.output/chrome-mv3/
npm run chrome:zip # store package
npm run chrome:test
npm run chrome:typecheck
```
Or inside this package:
```bash
npm install
npm run dev
npm run build
```
### Load unpacked
1. `npm run chrome:build`
2. Chrome → `chrome://extensions` → Developer Mode
3. **Load unpacked** → select `packages/chrome/.output/chrome-mv3`
## Shared core
Provider adapters and prompts live in [`../../src/lib`](../../src/lib) (`@lib/*`). Do not import `@lib/crypto` or `@lib/messaging` from the VS Code or CLI packages.
## Version
Bump `version` in this packages `package.json` only — the extension manifest follows it via `wxt.config.ts`.

7018
packages/chrome/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"name": "lexai-chrome",
"version": "1.1.0",
"description": "LexAI Chrome extension (Manifest V3) — BYO-LLM writing assistant",
"private": true,
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"dev": "wxt",
"build": "wxt build",
"zip": "wxt zip",
"postinstall": "wxt prepare",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
"test": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@wxt-dev/module-react": "^1.1.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tweetnacl": "^1.0.3",
"wxt": "^0.20.18"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@types/chrome": "^0.1.37",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"jsdom": "^28.1.0",
"typescript": "^5.7.3",
"vitest": "^3.0.7"
}
}

View File

@@ -1,10 +1,9 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
testDir: '../../tests/e2e',
timeout: 30000,
use: {
// Chrome extension testing
channel: 'chrome',
},
projects: [

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["chrome"],
"esModuleInterop": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"paths": {
"@lib/*": ["../../src/lib/*"]
}
},
"include": [
"entrypoints/**/*",
"../../src/lib/**/*",
".wxt/types/**/*"
],
"exclude": [
"node_modules",
".output"
]
}

View File

@@ -4,6 +4,7 @@
"types": ["chrome", "vitest/globals"]
},
"include": [
"tests/**/*"
"../../tests/**/*",
"../../src/lib/**/*"
]
}

View File

@@ -4,14 +4,14 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
resolve: {
alias: {
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
'@lib': fileURLToPath(new URL('../../src/lib', import.meta.url)),
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/unit/setup.ts'],
include: ['tests/unit/**/*.test.ts'],
exclude: ['tests/e2e/**/*'],
setupFiles: ['../../tests/unit/setup.ts'],
include: ['../../tests/unit/**/*.test.ts'],
exclude: ['../../tests/e2e/**/*'],
},
});

View File

@@ -5,16 +5,15 @@ import pkg from './package.json';
export default defineConfig({
extensionApi: 'chrome',
// Shared-code alias. Deliberately NOT "~" or "@" — WXT force-overwrites those
// to srcDir (the project root here), so they cannot point at ./src.
// to srcDir, so they cannot point at the monorepo shared lib.
alias: {
'@lib': resolve(__dirname, 'src/lib'),
'@lib': resolve(__dirname, '../../src/lib'),
},
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'LexAI - AI Writing Assistant',
description: 'Grammar checking and writing assistance powered by your own LLM API key',
// Single source of truth: manifest version always follows package.json,
// so a release bump is a one-file edit and versions can never drift.
// Single source of truth: manifest version always follows package.json.
version: pkg.version,
icons: {
'16': 'icon-16.png',

88
packages/cli/README.md Normal file
View File

@@ -0,0 +1,88 @@
# LexAI CLI — Prompt Builder
Part of the [LexAI monorepo](../../README.md) (`packages/cli`). Complements the [Chrome](../chrome/README.md) and [VS Code](../vscode/README.md) packages.
Improve a rough idea into a **paste-ready engineered prompt** before you fire it at Claude Code, Cursor, or another agent.
BYO-LLM only — no LexAI servers. Uses the same Prompt Builder core as the Chrome and VS Code extensions.
## Install (from this repo)
```bash
# from repo root
npm run cli:install
npm run cli:build
# make `lexai` available on your PATH
cd packages/cli && npm link
```
Or run without linking:
```bash
node packages/cli/out/cli.js prompt "your rough idea"
```
## Setup
```powershell
# PowerShell
$env:LEXAI_API_KEY = "sk-..."
$env:LEXAI_PROVIDER = "openai" # optional: openai | anthropic | groq | openrouter
$env:LEXAI_MODEL = "gpt-4o" # optional
```
Optional defaults file (no API key allowed here): `~/.lexai/config.json`
```json
{
"provider": "openai",
"model": "gpt-4o",
"pattern": "role",
"persona": "Expert Developer",
"format": "Markdown",
"style": "Concise"
}
```
Precedence: **flags > config file > env > defaults**. Key is always from `LEXAI_API_KEY`.
## Usage
```bash
lexai prompt "add rate limiting to the express auth routes"
echo "debug flaky playwright on CI" | lexai prompt --pattern role --persona "Expert Developer"
lexai prompt -f draft.txt --format Markdown --model gpt-4o
lexai prompt --list
lexai prompt --help
```
The engineered prompt is written to **stdout** (pipe-safe). Progress and errors go to **stderr**.
```powershell
# Windows: copy to clipboard
lexai prompt "write a unit test for parseConfig" | Set-Clipboard
```
### Flags
| Flag | Meaning |
| --- | --- |
| `-f, --file <path>` | Read input from file |
| `--pattern <id>` | Pattern id (`auto`, `role`, `cot`, …) — see `--list` |
| `--persona <name\|text>` | Persona preset or free text |
| `--format <name>` | Output format hint |
| `--style <name>` | Style the engineered prompt should request |
| `--provider` / `--model` | Provider overrides |
| `-l, --list` | List patterns, personas, formats, styles |
| `-h, --help` | Help |
Exit codes: `0` success · `1` user/config error · `2` provider/network error
## Scope
**In:** Make Prompt / Prompt Builder only.
**Out:** Fix/Rephrase/Code Assist, reading VS Code/Chrome keys, auto-running shell commands.

35
packages/cli/esbuild.mjs Normal file
View File

@@ -0,0 +1,35 @@
import * as esbuild from 'esbuild';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const watch = process.argv.includes('--watch');
const libRoot = resolve(__dirname, '../../src/lib');
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints: [resolve(__dirname, 'src/cli.ts')],
bundle: true,
outfile: resolve(__dirname, 'out/cli.js'),
format: 'esm',
platform: 'node',
target: 'node22',
sourcemap: true,
sourcesContent: false,
logLevel: 'info',
banner: {
js: '#!/usr/bin/env node',
},
alias: {
'@lib': libRoot,
},
};
if (watch) {
const ctx = await esbuild.context(options);
await ctx.watch();
console.log('[lexai-cli] watching…');
} else {
await esbuild.build(options);
console.log('[lexai-cli] compiled → out/cli.js');
}

572
packages/cli/out/cli.js Normal file
View File

@@ -0,0 +1,572 @@
#!/usr/bin/env node
// src/args.ts
function parseArgs(argv) {
const out = {
help: false,
list: false,
positionals: []
};
let i = 0;
while (i < argv.length) {
const a = argv[i];
if (a === "-h" || a === "--help") {
out.help = true;
i += 1;
continue;
}
if (a === "--list" || a === "-l") {
out.list = true;
i += 1;
continue;
}
if (a === "-f" || a === "--file") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --file");
out.file = v;
i += 1;
continue;
}
if (a === "--pattern") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --pattern");
out.pattern = v;
i += 1;
continue;
}
if (a === "--persona") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --persona");
out.persona = v;
i += 1;
continue;
}
if (a === "--format") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --format");
out.format = v;
i += 1;
continue;
}
if (a === "--style") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --style");
out.style = v;
i += 1;
continue;
}
if (a === "--provider") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --provider");
out.provider = v;
i += 1;
continue;
}
if (a === "--model") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --model");
out.model = v;
i += 1;
continue;
}
if (a.startsWith("-")) {
throw new Error(`Unknown flag: ${a}`);
}
if (!out.command) {
out.command = a;
} else {
out.positionals.push(a);
}
i += 1;
}
return out;
}
function usage() {
return `LexAI CLI \u2014 Prompt Builder
Improve a rough idea into a paste-ready prompt before you fire it.
Usage:
lexai prompt [options] "<rough idea>"
echo "..." | lexai prompt [options]
lexai prompt -f draft.txt [options]
Options:
-f, --file <path> Read input from file
--pattern <id> Prompt pattern (auto, role, cot, \u2026)
--persona <name|text> Persona preset or free text
--format <name> Output format (Markdown, JSON, \u2026)
--style <name> Style for the engineered prompt (Formal, Concise, \u2026)
--provider <id> openai | anthropic | groq | openrouter
--model <id> Model override
-l, --list List patterns, personas, formats
-h, --help Show help
Environment:
LEXAI_API_KEY Required API key (never stored in config file)
LEXAI_PROVIDER Default provider (default: openai)
LEXAI_MODEL Default model
Optional config file: ~/.lexai/config.json
{ "provider", "model", "pattern", "persona", "format", "style" }
Exit codes: 0 success \xB7 1 user/config error \xB7 2 provider/network error
`;
}
// src/prompt.ts
import { readFileSync as readFileSync2 } from "node:fs";
// ../../src/lib/actions.ts
var ACTIONS = ["fix", "rephrase", "shorten", "expand", "explain", "prompt"];
var ACTION_LABELS = {
fix: "Fix Grammar",
rephrase: "Rephrase",
shorten: "Shorten",
expand: "Expand",
explain: "Explain",
prompt: "Make Prompt"
};
var WRITING_STYLES = ["Default", "Formal", "Casual", "Academic", "Creative", "Concise"];
var CONTEXT_MENU_STYLES = WRITING_STYLES.filter((s) => s !== "Default");
var PROMPT_PATTERNS = [
{ id: "auto", label: "Auto", group: "Direct", hint: "Let the prompt engineer pick the cheapest pattern that fits the input." },
{ id: "zero-shot", label: "Zero-shot Instruction", group: "Direct", hint: "Direct imperative instructions, no scaffolding. Lowest cost \u2014 simple, single-step tasks." },
{ id: "role", label: "Role Conditioning", group: "Direct", hint: "Role/domain/invariants block; steadies tone and expertise across a longer chat." },
{ id: "few-shot", label: "Few-shot Examples", group: "Direct", hint: "Shows 1-2 example input\u2192output pairs. Best for consistent formatting or extraction." },
{ id: "structured", label: "Structured Sections", group: "Direct", hint: "Labeled Context/Task/Constraints/Output. Clearer for multi-constraint requests." },
{ id: "contract", label: "Output Contract", group: "Direct", hint: "Pins an exact output schema. Best when downstream code parses the result." },
{ id: "cot", label: "Chain-of-Thought", group: "Reasoning", hint: "Reasons step by step before answering. For math, logic, multi-constraint problems." },
{ id: "plan-solve", label: "Plan-and-Solve", group: "Reasoning", hint: "Plans first, then executes in order. For open-ended design or multi-part tasks." },
{ id: "tot", label: "Tree-of-Thoughts", group: "Reasoning", hint: "Scores multiple candidate approaches and keeps the best. High cost \u2014 hard planning/refactors." },
{ id: "react", label: "ReAct (tools)", group: "Agentic", hint: "Thought/Action/Observation tool loop for a tool-capable agent, not a plain chat." },
{ id: "pev", label: "Plan-Execute-Verify", group: "Agentic", hint: "Task DAG with per-step verification, for a tool-capable agent on multi-step builds." },
{ id: "gauntlet", label: "Gauntlet (Builder\u2013Judge)", group: "Agentic", hint: "Builder vs. fresh-context judge on a named standard, for a tool-capable agent. Very high cost." }
];
var LEGACY_PATTERN_IDS = {
Auto: "auto",
Instructional: "zero-shot",
"Role-play": "role",
"Step-by-step": "cot",
"Few-shot": "few-shot",
Structured: "structured"
};
function resolvePromptPattern(pattern, legacyStyle) {
if (pattern && PROMPT_PATTERNS.some((p) => p.id === pattern)) return pattern;
if (legacyStyle && LEGACY_PATTERN_IDS[legacyStyle]) return LEGACY_PATTERN_IDS[legacyStyle];
return "auto";
}
var PROMPT_PERSONAS = [
"Auto",
"None",
"Expert Developer",
"Copywriter",
"Teacher",
"Data Analyst",
"Business Consultant",
"Researcher",
"Custom\u2026"
];
var PROMPT_FORMATS = ["Auto", "Plain text", "Markdown", "Bulleted list", "Numbered steps", "JSON", "Table"];
var MIN_SELECTION_LENGTH = 10;
var CONTEXT_MENU_ENTRIES = ACTIONS.flatMap((action) => [
{
id: `lexai-${action}`,
action,
style: "Default",
title: `\u26A1 LexAI: ${ACTION_LABELS[action]}`
},
// 'prompt' opens the Prompt Builder dialog in the page, which has its own
// parameters — writing-style children don't apply to it.
...action === "prompt" ? [] : CONTEXT_MENU_STYLES.map((style) => ({
id: `lexai-${action}-${style.toLowerCase()}`,
action,
style,
parentId: `lexai-${action}`,
title: style
}))
]);
// ../../src/lib/providers.ts
async function fetchWithTimeout(url, options, timeoutMs = 3e4) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(id);
}
}
var PROMPT_BASE_TASK = "You are an expert prompt engineer. Transform the provided text into one well-crafted prompt that will get the best possible result from an AI model. First infer what the user is trying to achieve \u2014 the text may be a rough idea, a question, or a description of the output they want \u2014 then compose exactly one engineered prompt for that goal.";
var PROMPT_AUTO_RUBRIC = " Choose the structural pattern the goal actually needs: formatting or extraction favors few-shot examples or an output contract; logic or multi-constraint problems favor chain-of-thought; open-ended design favors plan-and-solve or tree-of-thoughts; tasks needing live data or tools favor ReAct; multi-step builds or migrations favor plan-execute-verify; tasks that must beat a quality bar favor a gauntlet builder-judge loop; otherwise use a plain zero-shot instruction. Prefer the cheapest pattern that meets the goal \u2014 never add reasoning scaffolding to a simple task.";
var PROMPT_PATTERN_INSTRUCTIONS = {
"zero-shot": " Compose the engineered prompt as direct, imperative instructions \u2014 one clear task per sentence, with no scaffolding beyond what the task actually needs.",
role: " Compose the engineered prompt as a system-role block with ROLE, DOMAIN, INVARIANT, and OUTPUT_FORMAT lines that assign the model its expertise and constraints; restate the single hardest constraint again as the very last line, to guard against it being forgotten in a long conversation (context decay).",
"few-shot": " Compose the engineered prompt using explicit <example><input>\u2026</input><output>\u2026</output></example> delimiters, with 1-2 examples that are structurally diverse from each other (not near-duplicates), to guard against the model overfitting to the last example's exact values (recency/label bias), then a trailing open <target><input>\u2026</input><output> for the real input.",
structured: " Compose the engineered prompt as labeled sections in this order: Context, Task, Constraints, Output format \u2014 each a short heading followed by its content.",
contract: " Compose the engineered prompt around an exact output schema: name every field, its type, and whether it is required, and include a rule instructing the model to reject or omit anything outside that schema.",
cot: " Compose the engineered prompt with two explicit phases labeled PHASE 1: REASONING and PHASE 2: OUTPUT \u2014 the model works through its reasoning in phase 1, then gives a final answer in phase 2 that stands alone without needing the reasoning to make sense.",
"plan-solve": " Compose the engineered prompt so the model must first produce a numbered plan of the steps needed, then execute that plan in order, referencing each step as it completes it.",
tot: " Compose the engineered prompt instructing the model to generate several candidate approaches, score each 0-1 against stated criteria, prune the weak ones, expand on the best, and report the winning approach and why it was chosen.",
react: " Compose the engineered prompt for a tool-capable agent, not a plain chat model: declare the available tools, require a strict Thought: / Action: / Observation: loop, and terminate with a line starting Final Answer:. Include a hard step budget (e.g. max 10 steps) and a rule that repeating the same action signature twice must break the loop, to guard against infinite loops.",
pev: " Compose the engineered prompt for a tool-capable agent, not a plain chat model: require it to first generate a task DAG of sub-tasks, run a verification assertion after each node, re-plan the remaining DAG on any failed assertion, and state an explicit stop condition for when the task is complete.",
gauntlet: " Compose the engineered prompt for a tool-capable agent, not a plain chat model, running a builder-judge loop: the builder produces an artifact, a judge instantiated in a fresh context compares it against a NAMED reference standard, and returns exactly STATUS: [PASS|FAIL] | FEEDBACK: <one directive>; the loop stops on PASS or after a stated maximum number of rounds."
};
function promptPatternSection(pattern) {
if (pattern && pattern !== "auto" && PROMPT_PATTERN_INSTRUCTIONS[pattern]) {
return PROMPT_PATTERN_INSTRUCTIONS[pattern];
}
return PROMPT_AUTO_RUBRIC;
}
function promptParamModifiers(params) {
if (!params) return "";
const parts = [];
if (params.persona === "None") {
parts.push(" Do not assign a persona or role in the engineered prompt.");
} else if (params.persona && params.persona !== "Auto") {
parts.push(` The engineered prompt must assign the model the persona of ${params.persona.trim()}, including the skills and expertise that persona implies.`);
}
if (params.format && params.format !== "Auto") {
parts.push(` The engineered prompt must require the final output as ${params.format.toLowerCase()}.`);
}
return parts.join("");
}
var PROMPT_INVARIANTS = " Return ONLY the engineered prompt, ready to paste into an AI chat \u2014 no explanations, no surrounding quotes, no preamble. Keep it self-contained, and use the cheapest structure that meets the goal. Format with clear line breaks and short labeled sections (not one long paragraph) so a human can read it easily. If the input is missing information the prompt needs, mark it as a [BRACKETED] placeholder rather than inventing facts.";
function getSystemPrompt(action, style, promptParams) {
const normalizedAction = action === "fix" ? "grammar" : action;
const prompts = {
grammar: "You are a professional grammar editor. Fix all grammar, spelling, and punctuation errors in the provided text. Preserve the original meaning and tone as closely as possible. Return ONLY the corrected text \u2014 no explanations, no preamble.",
rephrase: "You are a skilled writing assistant. Rephrase the provided text to make it clearer, more engaging, and more professional. Keep the same meaning and approximate length. Return ONLY the rephrased text \u2014 no explanations.",
shorten: "You are a concise editor. Shorten the provided text by at least 30% while preserving the core message. Remove filler words, redundant phrases, and unnecessary detail. Return ONLY the shortened text.",
expand: "You are an experienced writer. Expand the provided text with more detail, context, and supporting points. Make it richer and more informative while staying on topic. Return ONLY the expanded text.",
explain: "You are a helpful teacher. Explain the following text in simple, easy-to-understand language. Break down complex terms, jargon, or concepts so anyone can understand. Be concise but clear. Return only the explanation, no extra commentary.",
prompt: PROMPT_BASE_TASK
};
const base = prompts[normalizedAction] ?? prompts.grammar;
if (normalizedAction !== "prompt") {
const styleModifier2 = style && style !== "Default" ? ` Write in a ${style.toLowerCase()} style.` : "";
return base + styleModifier2;
}
const patternSection = promptPatternSection(promptParams?.pattern);
const modifiers = promptParamModifiers(promptParams);
const styleModifier = style && style !== "Default" ? ` The engineered prompt should instruct the model to respond in a ${style.toLowerCase()} style.` : "";
return base + patternSection + modifiers + styleModifier + PROMPT_INVARIANTS;
}
function bearerHeaders(apiKey) {
return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
}
function openAiStyleBody(temperature) {
return (model, systemPrompt, text, maxTokens) => ({
model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: text }
],
max_tokens: maxTokens,
...temperature !== void 0 ? { temperature } : {}
});
}
var extractOpenAiStyle = (data) => data?.choices?.[0]?.message?.content;
var OPENAI_REASONING_MODEL_RE = /^(o\d|gpt-5)/;
function openAiBody(model, systemPrompt, text, maxTokens) {
return {
model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: text }
],
max_completion_tokens: maxTokens,
...OPENAI_REASONING_MODEL_RE.test(model) ? {} : { temperature: 0.7 }
};
}
var PROVIDER_SPECS = {
openai: {
label: "OpenAI",
chatUrl: "https://api.openai.com/v1/chat/completions",
modelsUrl: "https://api.openai.com/v1/models",
defaultModel: "gpt-4o-mini",
headers: bearerHeaders,
body: openAiBody,
extract: extractOpenAiStyle
},
anthropic: {
label: "Anthropic",
chatUrl: "https://api.anthropic.com/v1/messages",
modelsUrl: "https://api.anthropic.com/v1/models",
defaultModel: "claude-3-5-haiku-20241022",
headers: (apiKey) => ({
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
// Anthropic rejects browser-origin requests unless this opt-in is sent.
// The service worker counts as a browser origin, so it's required here too.
"anthropic-dangerous-direct-browser-access": "true"
}),
body: (model, systemPrompt, text, maxTokens) => ({
model,
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: "user", content: text }]
}),
extract: (data) => data?.content?.[0]?.text
},
groq: {
label: "Groq",
chatUrl: "https://api.groq.com/openai/v1/chat/completions",
modelsUrl: "https://api.groq.com/openai/v1/models",
defaultModel: "llama-3.3-70b-versatile",
headers: bearerHeaders,
body: openAiStyleBody(0.7),
extract: extractOpenAiStyle
},
openrouter: {
label: "OpenRouter",
chatUrl: "https://openrouter.ai/api/v1/chat/completions",
modelsUrl: "https://openrouter.ai/api/v1/models",
defaultModel: "openai/gpt-4o-mini",
headers: (apiKey) => ({
...bearerHeaders(apiKey),
"HTTP-Referer": "https://lexai.dev",
"X-Title": "LexAI"
}),
body: openAiStyleBody(void 0),
extract: extractOpenAiStyle
}
};
var KEY_HINT = " \u2014 open LexAI Settings and re-enter your API key for this provider.";
var isAuthStatus = (status) => status === 401 || status === 403;
function defaultMaxTokens(text) {
return Math.max(1024, Math.min(8192, Math.ceil(text.length)));
}
async function callProvider(config, text, systemPrompt, opts) {
const provider = config.provider || "openai";
const spec = PROVIDER_SPECS[provider];
if (!spec) {
return { error: `Unknown provider: "${provider}". Please check LexAI settings.` };
}
const model = config.model || spec.defaultModel;
const maxTokens = opts?.maxTokens ?? defaultMaxTokens(text);
let res;
try {
res = await fetchWithTimeout(spec.chatUrl, {
method: "POST",
headers: spec.headers(config.apiKey ?? ""),
body: JSON.stringify(spec.body(model, systemPrompt, text, maxTokens))
});
} catch (err) {
return { error: `Network error reaching ${spec.label}: ${String(err)}` };
}
const data = await res.json().catch(() => null);
if (!res.ok) {
const msg = data?.error?.message ?? `HTTP ${res.status}`;
return { error: `${spec.label} error: ${msg}${isAuthStatus(res.status) ? KEY_HINT : ""}` };
}
const result = spec.extract(data);
if (!result) return { error: `${spec.label} returned an empty response.` };
return { result: result.trim() };
}
// src/config.ts
import { readFileSync, existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
function configPath() {
return join(homedir(), ".lexai", "config.json");
}
function loadFileConfig() {
const path = configPath();
if (!existsSync(path)) return {};
try {
const raw = readFileSync(path, "utf8");
const data = JSON.parse(raw);
if (data.apiKey !== void 0) {
throw new Error(
`${path} must not contain apiKey. Set LEXAI_API_KEY in the environment instead.`
);
}
return {
provider: typeof data.provider === "string" ? data.provider : void 0,
model: typeof data.model === "string" ? data.model : void 0,
pattern: typeof data.pattern === "string" ? data.pattern : void 0,
persona: typeof data.persona === "string" ? data.persona : void 0,
format: typeof data.format === "string" ? data.format : void 0,
style: typeof data.style === "string" ? data.style : void 0
};
} catch (err) {
if (err instanceof SyntaxError) {
throw new Error(`Invalid JSON in ${path}: ${err.message}`);
}
throw err;
}
}
function resolveConfig(flags) {
const file = loadFileConfig();
const apiKey = process.env.LEXAI_API_KEY?.trim();
if (!apiKey) {
throw new Error(
'LEXAI_API_KEY is not set. Export your provider API key, e.g.\n set LEXAI_API_KEY=sk-... (PowerShell: $env:LEXAI_API_KEY="sk-...")'
);
}
const provider = flags.provider || file.provider || process.env.LEXAI_PROVIDER?.trim() || "openai";
const model = flags.model || file.model || process.env.LEXAI_MODEL?.trim() || void 0;
return {
lexai: {
provider,
model,
apiKey
},
pattern: flags.pattern || file.pattern,
persona: flags.persona || file.persona,
format: flags.format || file.format,
style: flags.style || file.style || "Default"
};
}
// src/prompt.ts
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString("utf8");
}
async function resolveInput(flags) {
if (flags.file) {
try {
return readFileSync2(flags.file, "utf8");
} catch (err) {
throw new Error(`Cannot read file ${flags.file}: ${String(err)}`);
}
}
if (flags.positionals.length > 0) {
return flags.positionals.join(" ");
}
if (!process.stdin.isTTY) {
return readStdin();
}
throw new Error(
"No input. Pass text as arguments, pipe via stdin, or use -f/--file.\nRun: lexai prompt --help"
);
}
function printCatalog() {
const lines = [];
lines.push("Patterns (--pattern <id>):");
for (const p of PROMPT_PATTERNS) {
lines.push(` ${p.id.padEnd(14)} ${p.label} \u2014 ${p.hint}`);
}
lines.push("");
lines.push("Personas (--persona):");
for (const p of PROMPT_PERSONAS) {
if (p === "Custom\u2026") {
lines.push(" <any free text> (use instead of Custom\u2026)");
continue;
}
lines.push(` ${p}`);
}
lines.push("");
lines.push("Formats (--format):");
for (const f of PROMPT_FORMATS) {
lines.push(` ${f}`);
}
lines.push("");
lines.push("Styles (--style):");
for (const s of WRITING_STYLES) {
lines.push(` ${s}`);
}
process.stderr.write(lines.join("\n") + "\n");
}
async function runPrompt(flags) {
let text;
try {
text = (await resolveInput(flags)).trim();
} catch (err) {
return { ok: false, kind: "user", message: err instanceof Error ? err.message : String(err) };
}
if (text.length < MIN_SELECTION_LENGTH) {
return {
ok: false,
kind: "user",
message: `Input too short (need at least ${MIN_SELECTION_LENGTH} characters after trim).`
};
}
let resolved;
try {
resolved = resolveConfig(flags);
} catch (err) {
return { ok: false, kind: "user", message: err instanceof Error ? err.message : String(err) };
}
const pattern = resolvePromptPattern(resolved.pattern);
const promptParams = {
pattern,
persona: resolved.persona,
format: resolved.format
};
const style = resolved.style || "Default";
const systemPrompt = getSystemPrompt("prompt", style, promptParams);
process.stderr.write(
`LexAI: engineering prompt (${resolved.lexai.provider}${resolved.lexai.model ? ` / ${resolved.lexai.model}` : ""}, pattern=${pattern})\u2026
`
);
const response = await callProvider(resolved.lexai, text, systemPrompt, {
maxTokens: Math.max(2048, defaultMaxTokens(text))
});
if (response.error || !response.result) {
return {
ok: false,
kind: "provider",
message: response.error ?? "Empty response from provider."
};
}
return { ok: true, result: response.result.replace(/\r\n/g, "\n").trim() };
}
// src/cli.ts
async function main() {
let flags;
try {
flags = parseArgs(process.argv.slice(2));
} catch (err) {
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
`);
return 1;
}
if (flags.help) {
process.stderr.write(usage());
return 0;
}
if (flags.list && (!flags.command || flags.command === "prompt")) {
printCatalog();
return 0;
}
if (!flags.command) {
process.stderr.write(usage());
return 1;
}
if (flags.command !== "prompt") {
process.stderr.write(
`Unknown command: ${flags.command}
Only "prompt" is supported in this release.
`
);
process.stderr.write(usage());
return 1;
}
const result = await runPrompt(flags);
if (!result.ok) {
process.stderr.write(`LexAI: ${result.message}
`);
return result.kind === "provider" ? 2 : 1;
}
process.stdout.write(result.result.endsWith("\n") ? result.result : `${result.result}
`);
return 0;
}
main().then((code) => {
process.exitCode = code;
}).catch((err) => {
process.stderr.write(`LexAI: unexpected error: ${String(err)}
`);
process.exitCode = 2;
});
//# sourceMappingURL=cli.js.map

File diff suppressed because one or more lines are too long

538
packages/cli/package-lock.json generated Normal file
View File

@@ -0,0 +1,538 @@
{
"name": "lexai-cli",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lexai-cli",
"version": "1.0.0",
"bin": {
"lexai": "out/cli.js"
},
"devDependencies": {
"@types/node": "^22.13.4",
"esbuild": "^0.25.0",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@types/node": {
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

23
packages/cli/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "lexai-cli",
"version": "1.0.0",
"description": "LexAI Prompt Builder CLI — improve a rough idea into a paste-ready prompt before you fire it",
"private": true,
"type": "module",
"bin": {
"lexai": "./out/cli.js"
},
"engines": {
"node": ">=22"
},
"scripts": {
"compile": "node esbuild.mjs",
"build": "node esbuild.mjs",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.13.4",
"esbuild": "^0.25.0",
"typescript": "^5.7.3"
}
}

140
packages/cli/src/args.ts Normal file
View File

@@ -0,0 +1,140 @@
export interface CliFlags {
help: boolean;
list: boolean;
file?: string;
pattern?: string;
persona?: string;
format?: string;
style?: string;
provider?: string;
model?: string;
/** Remaining positional args after the subcommand */
positionals: string[];
/** First non-flag token (expected: prompt) */
command?: string;
}
/**
* Minimal argv parser for `lexai prompt [flags] [text…]`.
* Unknown flags → throw with a clear message.
*/
export function parseArgs(argv: string[]): CliFlags {
const out: CliFlags = {
help: false,
list: false,
positionals: [],
};
let i = 0;
// Skip node + script if present (when run via node out/cli.js)
// Caller should pass process.argv.slice(2).
while (i < argv.length) {
const a = argv[i];
if (a === '-h' || a === '--help') {
out.help = true;
i += 1;
continue;
}
if (a === '--list' || a === '-l') {
out.list = true;
i += 1;
continue;
}
if (a === '-f' || a === '--file') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --file');
out.file = v;
i += 1;
continue;
}
if (a === '--pattern') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --pattern');
out.pattern = v;
i += 1;
continue;
}
if (a === '--persona') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --persona');
out.persona = v;
i += 1;
continue;
}
if (a === '--format') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --format');
out.format = v;
i += 1;
continue;
}
if (a === '--style') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --style');
out.style = v;
i += 1;
continue;
}
if (a === '--provider') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --provider');
out.provider = v;
i += 1;
continue;
}
if (a === '--model') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --model');
out.model = v;
i += 1;
continue;
}
if (a.startsWith('-')) {
throw new Error(`Unknown flag: ${a}`);
}
if (!out.command) {
out.command = a;
} else {
out.positionals.push(a);
}
i += 1;
}
return out;
}
export function usage(): string {
return `LexAI CLI — Prompt Builder
Improve a rough idea into a paste-ready prompt before you fire it.
Usage:
lexai prompt [options] "<rough idea>"
echo "..." | lexai prompt [options]
lexai prompt -f draft.txt [options]
Options:
-f, --file <path> Read input from file
--pattern <id> Prompt pattern (auto, role, cot, …)
--persona <name|text> Persona preset or free text
--format <name> Output format (Markdown, JSON, …)
--style <name> Style for the engineered prompt (Formal, Concise, …)
--provider <id> openai | anthropic | groq | openrouter
--model <id> Model override
-l, --list List patterns, personas, formats
-h, --help Show help
Environment:
LEXAI_API_KEY Required API key (never stored in config file)
LEXAI_PROVIDER Default provider (default: openai)
LEXAI_MODEL Default model
Optional config file: ~/.lexai/config.json
{ "provider", "model", "pattern", "persona", "format", "style" }
Exit codes: 0 success · 1 user/config error · 2 provider/network error
`;
}

53
packages/cli/src/cli.ts Normal file
View File

@@ -0,0 +1,53 @@
import { parseArgs, usage } from './args.js';
import { printCatalog, runPrompt } from './prompt.js';
async function main(): Promise<number> {
let flags;
try {
flags = parseArgs(process.argv.slice(2));
} catch (err) {
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
return 1;
}
if (flags.help) {
process.stderr.write(usage());
return 0;
}
if (flags.list && (!flags.command || flags.command === 'prompt')) {
printCatalog();
return 0;
}
if (!flags.command) {
process.stderr.write(usage());
return 1;
}
if (flags.command !== 'prompt') {
process.stderr.write(
`Unknown command: ${flags.command}\nOnly "prompt" is supported in this release.\n\n`,
);
process.stderr.write(usage());
return 1;
}
const result = await runPrompt(flags);
if (!result.ok) {
process.stderr.write(`LexAI: ${result.message}\n`);
return result.kind === 'provider' ? 2 : 1;
}
process.stdout.write(result.result.endsWith('\n') ? result.result : `${result.result}\n`);
return 0;
}
main()
.then((code) => {
process.exitCode = code;
})
.catch((err) => {
process.stderr.write(`LexAI: unexpected error: ${String(err)}\n`);
process.exitCode = 2;
});

View File

@@ -0,0 +1,93 @@
import { readFileSync, existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { LexAIConfig } from '@lib/types';
import type { WritingStyle } from '@lib/actions';
import type { CliFlags } from './args.js';
export interface FileConfig {
provider?: string;
model?: string;
pattern?: string;
persona?: string;
format?: string;
style?: string;
}
export interface ResolvedCliConfig {
lexai: LexAIConfig;
pattern?: string;
persona?: string;
format?: string;
style?: WritingStyle | string;
}
function configPath(): string {
return join(homedir(), '.lexai', 'config.json');
}
export function loadFileConfig(): FileConfig {
const path = configPath();
if (!existsSync(path)) return {};
try {
const raw = readFileSync(path, 'utf8');
const data = JSON.parse(raw) as FileConfig & { apiKey?: unknown };
if (data.apiKey !== undefined) {
// Never accept keys from disk — refuse loudly so users move to env.
throw new Error(
`${path} must not contain apiKey. Set LEXAI_API_KEY in the environment instead.`,
);
}
return {
provider: typeof data.provider === 'string' ? data.provider : undefined,
model: typeof data.model === 'string' ? data.model : undefined,
pattern: typeof data.pattern === 'string' ? data.pattern : undefined,
persona: typeof data.persona === 'string' ? data.persona : undefined,
format: typeof data.format === 'string' ? data.format : undefined,
style: typeof data.style === 'string' ? data.style : undefined,
};
} catch (err) {
if (err instanceof SyntaxError) {
throw new Error(`Invalid JSON in ${path}: ${err.message}`);
}
throw err;
}
}
/**
* Precedence for non-secret fields: flags > config file > env > defaults.
* API key: environment only.
*/
export function resolveConfig(flags: CliFlags): ResolvedCliConfig {
const file = loadFileConfig();
const apiKey = process.env.LEXAI_API_KEY?.trim();
if (!apiKey) {
throw new Error(
'LEXAI_API_KEY is not set. Export your provider API key, e.g.\n set LEXAI_API_KEY=sk-... (PowerShell: $env:LEXAI_API_KEY="sk-...")',
);
}
const provider =
flags.provider ||
file.provider ||
process.env.LEXAI_PROVIDER?.trim() ||
'openai';
const model =
flags.model ||
file.model ||
process.env.LEXAI_MODEL?.trim() ||
undefined;
return {
lexai: {
provider,
model,
apiKey,
},
pattern: flags.pattern || file.pattern,
persona: flags.persona || file.persona,
format: flags.format || file.format,
style: flags.style || file.style || 'Default',
};
}

131
packages/cli/src/prompt.ts Normal file
View File

@@ -0,0 +1,131 @@
import { readFileSync } from 'node:fs';
import {
MIN_SELECTION_LENGTH,
PROMPT_FORMATS,
PROMPT_PATTERNS,
PROMPT_PERSONAS,
WRITING_STYLES,
resolvePromptPattern,
type WritingStyle,
} from '@lib/actions';
import { callProvider, defaultMaxTokens, getSystemPrompt } from '@lib/providers';
import type { PromptParams } from '@lib/types';
import type { CliFlags } from './args.js';
import { resolveConfig } from './config.js';
async function readStdin(): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString('utf8');
}
/**
* Input order: --file > positional args > stdin (when not a TTY).
*/
export async function resolveInput(flags: CliFlags): Promise<string> {
if (flags.file) {
try {
return readFileSync(flags.file, 'utf8');
} catch (err) {
throw new Error(`Cannot read file ${flags.file}: ${String(err)}`);
}
}
if (flags.positionals.length > 0) {
return flags.positionals.join(' ');
}
if (!process.stdin.isTTY) {
return readStdin();
}
throw new Error(
'No input. Pass text as arguments, pipe via stdin, or use -f/--file.\nRun: lexai prompt --help',
);
}
export function printCatalog(): void {
const lines: string[] = [];
lines.push('Patterns (--pattern <id>):');
for (const p of PROMPT_PATTERNS) {
lines.push(` ${p.id.padEnd(14)} ${p.label}${p.hint}`);
}
lines.push('');
lines.push('Personas (--persona):');
for (const p of PROMPT_PERSONAS) {
if (p === 'Custom…') {
lines.push(' <any free text> (use instead of Custom…)');
continue;
}
lines.push(` ${p}`);
}
lines.push('');
lines.push('Formats (--format):');
for (const f of PROMPT_FORMATS) {
lines.push(` ${f}`);
}
lines.push('');
lines.push('Styles (--style):');
for (const s of WRITING_STYLES) {
lines.push(` ${s}`);
}
process.stderr.write(lines.join('\n') + '\n');
}
export type PromptRunResult =
| { ok: true; result: string }
| { ok: false; kind: 'user' | 'provider'; message: string };
export async function runPrompt(flags: CliFlags): Promise<PromptRunResult> {
let text: string;
try {
text = (await resolveInput(flags)).trim();
} catch (err) {
return { ok: false, kind: 'user', message: err instanceof Error ? err.message : String(err) };
}
if (text.length < MIN_SELECTION_LENGTH) {
return {
ok: false,
kind: 'user',
message: `Input too short (need at least ${MIN_SELECTION_LENGTH} characters after trim).`,
};
}
let resolved;
try {
resolved = resolveConfig(flags);
} catch (err) {
return { ok: false, kind: 'user', message: err instanceof Error ? err.message : String(err) };
}
const pattern = resolvePromptPattern(resolved.pattern);
const promptParams: PromptParams = {
pattern,
persona: resolved.persona,
format: resolved.format,
};
const style = (resolved.style || 'Default') as WritingStyle;
const systemPrompt = getSystemPrompt('prompt', style, promptParams);
process.stderr.write(
`LexAI: engineering prompt (${resolved.lexai.provider}${resolved.lexai.model ? ` / ${resolved.lexai.model}` : ''}, pattern=${pattern})…\n`,
);
const response = await callProvider(resolved.lexai, text, systemPrompt, {
maxTokens: Math.max(2048, defaultMaxTokens(text)),
});
if (response.error || !response.result) {
return {
ok: false,
kind: 'provider',
message: response.error ?? 'Empty response from provider.',
};
}
return { ok: true, result: response.result.replace(/\r\n/g, '\n').trim() };
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM"],
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true,
"types": ["node"],
"baseUrl": ".",
"paths": {
"@lib/*": ["../../src/lib/*"]
}
},
"include": [
"src/**/*",
"../../src/lib/actions.ts",
"../../src/lib/providers.ts",
"../../src/lib/types.ts"
],
"exclude": ["node_modules", "out"]
}

View File

@@ -1,4 +1,6 @@
# LexAI — AI Writing & Code Assist
# LexAI for VS Code / Cursor
Part of the [LexAI monorepo](../../README.md) (`packages/vscode`). Twin of the [Chrome extension](../chrome/README.md) and [CLI Prompt Builder](../cli/README.md).
**Bring your own LLM key.** LexAI helps you write and work with code in VS Code / Cursor — no LexAI account, no subscription, no LexAI servers.

View File

@@ -4,25 +4,15 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["chrome"],
"esModuleInterop": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"paths": {
"@lib/*": ["./src/lib/*"]
}
},
"include": [
"entrypoints/**/*",
"src/**/*",
".wxt/types/**/*"
],
"exclude": [
"node_modules",
".output"
]
"include": ["src/lib/**/*"],
"exclude": ["node_modules", "packages"]
}