feat(cli): add initial CLI implementation with argument parsing and prompt handling
- 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:
48
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
48
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal 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.).
|
||||||
24
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
24
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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): #
|
||||||
@@ -29,32 +29,30 @@ jobs:
|
|||||||
git checkout ${{ gitea.sha }}
|
git checkout ${{ gitea.sha }}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install Chrome package dependencies
|
||||||
run: npm ci --prefer-offline --no-audit --no-fund
|
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.
|
# 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
|
- name: Prepare WXT types
|
||||||
run: npx wxt prepare
|
run: npx wxt prepare
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test -- --run
|
run: npm test -- --run
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Build extension
|
- name: Build extension
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Verify build output
|
- name: Verify build output
|
||||||
run: ls -la .output/chrome-mv3/
|
run: ls -la .output/chrome-mv3/
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Package as ZIP
|
- name: Package as ZIP
|
||||||
run: |
|
run: |
|
||||||
@@ -64,7 +62,7 @@ jobs:
|
|||||||
zip -r /tmp/lexai-chrome-mv3-${VERSION}.zip .output/chrome-mv3/
|
zip -r /tmp/lexai-chrome-mv3-${VERSION}.zip .output/chrome-mv3/
|
||||||
echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
|
echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
|
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
|
||||||
working-directory: /tmp/lexai
|
working-directory: /tmp/lexai/packages/chrome
|
||||||
|
|
||||||
- name: Publish to Gitea Package Registry
|
- name: Publish to Gitea Package Registry
|
||||||
if: gitea.event_name == 'push'
|
if: gitea.event_name == 'push'
|
||||||
|
|||||||
@@ -30,24 +30,29 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
# Same gates as CI — a release must never check less than a push.
|
# Same gates as CI — a release must never check less than a push.
|
||||||
- name: Prepare WXT types
|
- name: Prepare WXT types
|
||||||
run: npx wxt prepare
|
run: npx wxt prepare
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test -- --run
|
run: npm test -- --run
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Build extension
|
- name: Build extension
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Package as ZIP
|
- name: Package as ZIP
|
||||||
run: |
|
run: |
|
||||||
VERSION=${{ gitea.ref_name || 'manual' }}
|
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
|
# Verify manifest exists and check version
|
||||||
node -e "console.log('Manifest version:', require('./.output/chrome-mv3/manifest.json').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
|
# 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 "ZIP_FILE=${ZIPFILE}" >> $GITHUB_ENV
|
||||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
|
echo "✅ Packaged: lexai-chrome-mv3-${VERSION}.zip"
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
|
|
||||||
- name: Get Chrome Web Store OAuth2 Token
|
- name: Get Chrome Web Store OAuth2 Token
|
||||||
|
|||||||
@@ -23,21 +23,26 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test -- --run
|
run: npm test -- --run
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Build extension
|
- name: Build extension
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Package preview ZIP
|
- name: Package preview ZIP
|
||||||
run: |
|
run: |
|
||||||
PR_NUM=${{ gitea.event.pull_request.number }}
|
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"
|
echo "✅ PR #${PR_NUM} build successful"
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Comment on PR
|
- name: Comment on PR
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -23,20 +23,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test -- --run
|
run: npm test -- --run
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Build extension
|
- name: Build extension
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Package as ZIP
|
- name: Package as ZIP
|
||||||
run: |
|
run: |
|
||||||
VERSION=${{ gitea.ref_name }}
|
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
|
# Zip from inside the chrome-mv3 dir so manifest.json is at root of the archive
|
||||||
cd .output/chrome-mv3 && zip -r "$ZIPFILE" . && cd -
|
cd .output/chrome-mv3 && zip -r "$ZIPFILE" . && cd -
|
||||||
echo "ZIP_FILE=${ZIPFILE}" >> $GITHUB_ENV
|
echo "ZIP_FILE=${ZIPFILE}" >> $GITHUB_ENV
|
||||||
|
working-directory: packages/chrome
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,9 +1,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.output/
|
.output/
|
||||||
.wxt/
|
.wxt/
|
||||||
|
packages/*/.output/
|
||||||
|
packages/*/.wxt/
|
||||||
dist/
|
dist/
|
||||||
*.local
|
*.local
|
||||||
*.zip
|
*.zip
|
||||||
|
*.vsix
|
||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
test-results/
|
test-results/
|
||||||
|
|||||||
20
AGENTS.md
20
AGENTS.md
@@ -10,16 +10,16 @@
|
|||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Project | LexAI — BYO-LLM writing help (Chrome MV3 + VS Code) |
|
| Project | LexAI — BYO-LLM writing help (Chrome MV3 + VS Code + CLI) |
|
||||||
| Outcome | Select text → AI action (fix/rephrase/shorten/expand/explain/prompt) → replace in place (Chrome also Copy), with no backend and no subscription |
|
| Outcome | Select text/code → AI help without 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; VS Code v1 skips floating toolbar / Prompt Builder / Copy As |
|
| 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 help without a SaaS subscription |
|
| Primary user | people who hold an LLM API key and want inline writing / prompt help |
|
||||||
| Acceptance tests | Chrome: `typecheck` + `test -- --run` + loadable `.output/chrome-mv3/` + encrypted key path; VS Code: `vscode:typecheck` + `vscode:build` + Secret Storage key + selection replace |
|
| Acceptance tests | `chrome:typecheck` + `chrome:test` + loadable `packages/chrome/.output/chrome-mv3/`; `vscode:typecheck` + `vscode:build`; `cli:typecheck` + `cli:build` |
|
||||||
| Constraints | WXT ^0.20 + React 18 + TS (Chrome); `packages/vscode` + esbuild; shared `src/lib`; Node 22; Gitea CI + Chrome Web Store |
|
| Constraints | Monorepo `packages/{chrome,vscode,cli}` + `src/lib`; Node 22; Gitea CI + Chrome Web Store |
|
||||||
| Source of truth | this file + `CLAUDE.md` + `docs/`; Plane (LEXAI); `docs/TASKS.md` |
|
| Source of truth | this file + `CLAUDE.md` + `docs/` + root `README.md`; Plane (LEXAI) |
|
||||||
| Reference bar | not yet concrete — proposal in `docs/REFERENCE_BAR.md`; no gauntlet until artifacts exist |
|
| Reference bar | proposal in `docs/REFERENCE_BAR.md`; no gauntlet until artifacts exist |
|
||||||
| Model routing | `docs/MODEL_ROUTING.md` — filled 2026-08-13 |
|
| Model routing | `docs/MODEL_ROUTING.md` |
|
||||||
| Commands | `install: npm install` · `test: npm test -- --run` · `lint: npm run typecheck` · `build: npm run build` · `vscode:build` / `vscode:typecheck` / `vscode:package` |
|
| Commands | `install:all` · `chrome:*` · `vscode:*` · `cli:*` |
|
||||||
|
|
||||||
### Definition of done
|
### Definition of done
|
||||||
|
|
||||||
|
|||||||
71
CLAUDE.md
71
CLAUDE.md
@@ -12,15 +12,15 @@
|
|||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Project | LexAI — Grammarly-like Chrome extension (Manifest V3), BYO-LLM-key |
|
| Project | LexAI — BYO-LLM Chrome MV3 + VS Code + CLI Prompt Builder |
|
||||||
| Outcome | Select text on any page → AI action (fix/rephrase/shorten/expand/explain/prompt) → Replace or Copy, with no backend and no subscription |
|
| 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 |
|
| 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 |
|
| Primary user | people who hold an LLM API key and want inline writing / prompt 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 |
|
| 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 | WXT ^0.20 + React 18 + TS; Node 22; Tailwind inactive (inline styles); `<all_urls>` today; Gitea CI + Chrome Web Store |
|
| 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/`; issue tracking in Plane (LEXAI); task list `docs/TASKS.md` (derived from `RECOMMENDATIONS.md`) |
|
| Source of truth | this file + `docs/` + root `README.md`; Plane (LEXAI); `docs/TASKS.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 |
|
| Reference bar | decision-ready proposals in `docs/REFERENCE_BAR.md` |
|
||||||
| 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) |
|
| Commands | `install:all` · `chrome:dev|build|test|typecheck|zip` · `vscode:*` · `cli:*` |
|
||||||
|
|
||||||
### Definition of done
|
### 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.
|
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`).
|
- **Providers:** OpenAI, Anthropic, Groq, OpenRouter (`packages/chrome/entrypoints/background.ts`).
|
||||||
- **No subscription, no server.** The API key lives encrypted in `chrome.storage.local`.
|
- **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
|
#### 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.
|
- **React 18** + TypeScript — Options and Popup pages only.
|
||||||
- **Zustand** — a dependency, but state is currently local; not yet wired into a store.
|
- **tweetnacl** — `secretbox` for the Chrome API key.
|
||||||
- **tweetnacl** / **tweetnacl-util** — `secretbox` symmetric encryption for the API key.
|
- **Vitest** / **Playwright** — configured in `packages/chrome`.
|
||||||
- **Vitest** (jsdom) unit tests, **Playwright** e2e.
|
- **Tailwind** inactive — inline styles only.
|
||||||
- **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.
|
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install # first-time setup (node_modules gitignored; not present by default)
|
npm run install:all
|
||||||
npm run dev # WXT dev server with hot reload
|
npm run chrome:dev
|
||||||
npm run build # production build → .output/chrome-mv3/
|
npm run chrome:build # → packages/chrome/.output/chrome-mv3/
|
||||||
npm run zip # package for Chrome Web Store
|
npm run chrome:zip
|
||||||
npm test # vitest (add `-- --run` for one-shot, non-watch)
|
npm run chrome:test
|
||||||
npm run test:e2e # Playwright (requires a prior `npm run build`)
|
npm run chrome:typecheck
|
||||||
npm run typecheck # tsc --noEmit
|
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`.
|
**Prerequisite:** Node **22**. Chrome `postinstall` runs `wxt prepare`. Load unpacked from `packages/chrome/.output/chrome-mv3`.
|
||||||
|
|
||||||
**Load unpacked in Chrome:** `npm run build` → `chrome://extensions` → Developer Mode → Load unpacked → select `.output/chrome-mv3`.
|
|
||||||
|
|
||||||
#### Architecture
|
#### Architecture
|
||||||
|
|
||||||
Three cooperating contexts, message-passed over `chrome.runtime`:
|
Chrome: three contexts over `chrome.runtime` under `packages/chrome/entrypoints/` (`content`, `background`, `options`, `popup`). VS Code and CLI reuse `@lib` from `src/lib`.
|
||||||
|
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
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
132
CODE_OF_CONDUCT.md
Normal 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
62
CONTRIBUTING.md
Normal 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
21
LICENSE
Normal 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.
|
||||||
95
README.md
95
README.md
@@ -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)
|
| Package | What it is | Docs |
|
||||||
- ✅ Grammar & spelling correction
|
| --- | --- | --- |
|
||||||
- ✅ Rephrase / rewrite text
|
| **[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) |
|
||||||
- ✅ Shorten or expand text
|
| **[packages/vscode](packages/vscode)** | VS Code / Cursor extension — writing actions + workspace-aware **Code Assist** | [README](packages/vscode/README.md) · [Deploy](packages/vscode/DEPLOY.md) |
|
||||||
- 🔜 Tone suggestions
|
| **[packages/cli](packages/cli)** | CLI **Prompt Builder** — improve a rough idea into a paste-ready prompt | [README](packages/cli/README.md) |
|
||||||
- 🔜 Autocomplete
|
|
||||||
- 🔜 Custom style profiles
|
|
||||||
|
|
||||||
## Supported LLM Providers
|
Shared provider/prompt core: [`src/lib`](src/lib).
|
||||||
- **OpenAI** (GPT-4o, GPT-4o-mini)
|
|
||||||
- **Anthropic** (Claude 3.5 Haiku, Sonnet)
|
|
||||||
- **Groq** (free tier — fast!)
|
|
||||||
- **OpenRouter** (100+ models)
|
|
||||||
|
|
||||||
## Tech Stack
|
## Quick start
|
||||||
- WXT + React 18 + TypeScript
|
|
||||||
- Manifest V3
|
|
||||||
- Inline-styled UI (no CSS framework)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
# Prerequisites: Node 22+
|
||||||
npm run dev # Dev mode with hot reload
|
git clone https://git.juankibin.space/kibin/LexAI.git
|
||||||
npm run build # Production build
|
cd LexAI
|
||||||
npm run zip # Package for Chrome Web Store
|
|
||||||
npm test # Run unit tests
|
# 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
|
## Repository layout
|
||||||
1. Run `npm run build`
|
|
||||||
2. Open Chrome → `chrome://extensions`
|
|
||||||
3. Enable **Developer Mode**
|
|
||||||
4. Click **Load unpacked** → select `.output/chrome-mv3`
|
|
||||||
|
|
||||||
## Project Management
|
```text
|
||||||
Plane: LEXAI project → https://plane-pro.juankibin.space
|
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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
- **Outcome:** VSIX install built; deploy guide written.
|
- **Outcome:** Monorepo OSS layout — Chrome in `packages/chrome`; VS Code + CLI alongside; root LICENSE/README/CONTRIBUTING/CODE_OF_CONDUCT; Gitea issue/PR templates.
|
||||||
- **Artifact:** `packages/vscode/lexai-vscode-0.1.0.vsix` (~44 KB). Guide: `packages/vscode/DEPLOY.md`.
|
- **Verified:** run `npm run chrome:install` + typecheck/test/build after pull (session in progress).
|
||||||
- **Verified:** `npm run package` in `packages/vscode` succeeded.
|
- **CI:** workflows use `packages/chrome` for install/build/zip paths.
|
||||||
- **Next smallest action:** Install from VSIX locally; for public store, create Marketplace publisher + `vsce publish` (set `private: false` first).
|
- **Next smallest action:** `npm run chrome:install && npm run chrome:typecheck && npm run chrome:test && npm run chrome:build` then commit when ready.
|
||||||
|
|||||||
@@ -4,19 +4,20 @@
|
|||||||
|
|
||||||
## Verified facts
|
## Verified facts
|
||||||
|
|
||||||
- LexAI has two clients: Chrome MV3 (repo root / WXT) and VS Code (`packages/vscode`).
|
- LexAI monorepo: `packages/chrome` (WXT), `packages/vscode`, `packages/cli`; shared `src/lib`.
|
||||||
- Portable core: `src/lib/providers.ts`, `actions.ts`, `types.ts`. Chrome-only: `crypto.ts`, `messaging.ts`.
|
- Portable core: `providers.ts`, `actions.ts`, `types.ts`. Chrome-only: `crypto.ts`, `messaging.ts`.
|
||||||
- Providers: OpenAI, Anthropic, Groq, OpenRouter — routed by `callProvider` / `PROVIDER_SPECS`.
|
- Providers: OpenAI, Anthropic, Groq, OpenRouter — `callProvider` / `PROVIDER_SPECS`.
|
||||||
- VS Code v1: Command Palette + editor context submenu; API key in Secret Storage; prefs `lexai.provider|model|writingStyle`.
|
- VS Code: Secret Storage; Code Assist; prefs `lexai.*`.
|
||||||
- Selection minimum length for actions: `MIN_SELECTION_LENGTH` (10) in `src/lib/actions.ts`.
|
- CLI: `lexai prompt`; `LEXAI_API_KEY`; optional `~/.lexai/config.json` (no apiKey).
|
||||||
- Chrome message listener must `return true` for async replies; snapshot selection before `await`.
|
- 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
|
## Conventions
|
||||||
|
|
||||||
- Style Chrome UI with inline styles (Tailwind not wired).
|
- Style Chrome UI with inline styles (Tailwind not wired).
|
||||||
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
|
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
|
||||||
- VS Code must not import `@lib/crypto` or `@lib/messaging`.
|
- VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`.
|
||||||
- Root scripts: `vscode:install`, `vscode:build`, `vscode:typecheck`, `vscode:package`.
|
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`.
|
||||||
|
|
||||||
## Environment quirks
|
## Environment quirks
|
||||||
|
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
|
|
||||||
| What | Where |
|
| What | Where |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Chrome content / background / options / popup | `entrypoints/` |
|
| Chrome entrypoints | `packages/chrome/entrypoints/` |
|
||||||
| Shared lib | `src/lib/` |
|
| Shared lib | `src/lib/` |
|
||||||
| VS Code extension | `packages/vscode/src/extension.ts` |
|
| VS Code | `packages/vscode/` |
|
||||||
| VS Code bundle | `packages/vscode/out/extension.js` |
|
| CLI | `packages/cli/` |
|
||||||
| Model lanes | `docs/MODEL_ROUTING.md` |
|
| 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
|
## Expiring notes
|
||||||
|
|
||||||
|
|||||||
47
package.json
47
package.json
@@ -1,38 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "lexai",
|
"name": "lexai",
|
||||||
"version": "1.1.0",
|
"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": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@11.13.0",
|
"packageManager": "npm@11.13.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"chrome:install": "npm install --prefix packages/chrome",
|
||||||
"build": "wxt build",
|
"chrome:dev": "npm run dev --prefix packages/chrome",
|
||||||
"zip": "wxt zip",
|
"chrome:build": "npm run build --prefix packages/chrome",
|
||||||
"test": "vitest",
|
"chrome:zip": "npm run zip --prefix packages/chrome",
|
||||||
"test:e2e": "playwright test",
|
"chrome:typecheck": "npm run typecheck --prefix packages/chrome",
|
||||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
"chrome:test": "npm test --prefix packages/chrome -- --run",
|
||||||
"postinstall": "wxt prepare",
|
"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:install": "npm install --prefix packages/vscode",
|
||||||
"vscode:build": "npm run compile --prefix packages/vscode",
|
"vscode:build": "npm run compile --prefix packages/vscode",
|
||||||
"vscode:typecheck": "npm run typecheck --prefix packages/vscode",
|
"vscode:typecheck": "npm run typecheck --prefix packages/vscode",
|
||||||
"vscode:package": "npm run package --prefix packages/vscode"
|
"vscode:package": "npm run package --prefix packages/vscode",
|
||||||
},
|
"cli:install": "npm install --prefix packages/cli",
|
||||||
"dependencies": {
|
"cli:build": "npm run compile --prefix packages/cli",
|
||||||
"@wxt-dev/module-react": "^1.1.5",
|
"cli:typecheck": "npm run typecheck --prefix packages/cli",
|
||||||
"react": "^18.3.1",
|
"install:all": "npm run chrome:install && npm run vscode:install && npm run cli:install",
|
||||||
"react-dom": "^18.3.1",
|
"typecheck:all": "npm run chrome:typecheck && npm run vscode:typecheck && npm run cli:typecheck",
|
||||||
"tweetnacl": "^1.0.3",
|
"build:all": "npm run chrome:build && npm run vscode:build && npm run cli:build"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
packages/chrome/README.md
Normal file
49
packages/chrome/README.md
Normal 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 package’s `package.json` only — the extension manifest follows it via `wxt.config.ts`.
|
||||||
7018
packages/chrome/package-lock.json
generated
Normal file
7018
packages/chrome/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
packages/chrome/package.json
Normal file
35
packages/chrome/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests/e2e',
|
testDir: '../../tests/e2e',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
use: {
|
use: {
|
||||||
// Chrome extension testing
|
|
||||||
channel: 'chrome',
|
channel: 'chrome',
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
28
packages/chrome/tsconfig.json
Normal file
28
packages/chrome/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"types": ["chrome", "vitest/globals"]
|
"types": ["chrome", "vitest/globals"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"tests/**/*"
|
"../../tests/**/*",
|
||||||
|
"../../src/lib/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,14 @@ import { defineConfig } from 'vitest/config';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
|
'@lib': fileURLToPath(new URL('../../src/lib', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./tests/unit/setup.ts'],
|
setupFiles: ['../../tests/unit/setup.ts'],
|
||||||
include: ['tests/unit/**/*.test.ts'],
|
include: ['../../tests/unit/**/*.test.ts'],
|
||||||
exclude: ['tests/e2e/**/*'],
|
exclude: ['../../tests/e2e/**/*'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -5,16 +5,15 @@ import pkg from './package.json';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
extensionApi: 'chrome',
|
extensionApi: 'chrome',
|
||||||
// Shared-code alias. Deliberately NOT "~" or "@" — WXT force-overwrites those
|
// 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: {
|
alias: {
|
||||||
'@lib': resolve(__dirname, 'src/lib'),
|
'@lib': resolve(__dirname, '../../src/lib'),
|
||||||
},
|
},
|
||||||
modules: ['@wxt-dev/module-react'],
|
modules: ['@wxt-dev/module-react'],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'LexAI - AI Writing Assistant',
|
name: 'LexAI - AI Writing Assistant',
|
||||||
description: 'Grammar checking and writing assistance powered by your own LLM API key',
|
description: 'Grammar checking and writing assistance powered by your own LLM API key',
|
||||||
// Single source of truth: manifest version always follows package.json,
|
// Single source of truth: manifest version always follows package.json.
|
||||||
// so a release bump is a one-file edit and versions can never drift.
|
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
icons: {
|
icons: {
|
||||||
'16': 'icon-16.png',
|
'16': 'icon-16.png',
|
||||||
|
|||||||
88
packages/cli/README.md
Normal file
88
packages/cli/README.md
Normal 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
35
packages/cli/esbuild.mjs
Normal 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
572
packages/cli/out/cli.js
Normal 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
|
||||||
6
packages/cli/out/cli.js.map
Normal file
6
packages/cli/out/cli.js.map
Normal file
File diff suppressed because one or more lines are too long
538
packages/cli/package-lock.json
generated
Normal file
538
packages/cli/package-lock.json
generated
Normal 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
23
packages/cli/package.json
Normal 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
140
packages/cli/src/args.ts
Normal 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
53
packages/cli/src/cli.ts
Normal 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;
|
||||||
|
});
|
||||||
93
packages/cli/src/config.ts
Normal file
93
packages/cli/src/config.ts
Normal 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
131
packages/cli/src/prompt.ts
Normal 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() };
|
||||||
|
}
|
||||||
25
packages/cli/tsconfig.json
Normal file
25
packages/cli/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
@@ -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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,15 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["chrome"],
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@lib/*": ["./src/lib/*"]
|
"@lib/*": ["./src/lib/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/lib/**/*"],
|
||||||
"entrypoints/**/*",
|
"exclude": ["node_modules", "packages"]
|
||||||
"src/**/*",
|
|
||||||
".wxt/types/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
".output"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user