Files
LexAI/packages/cli/out/cli.js
john kevin asprec 2c00d2e431
Some checks failed
CI — Test & Build / Test & Build (pull_request) Has been cancelled
Preview — PR Build Check / PR Preview Build (pull_request) Has been cancelled
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.
2026-08-13 19:19:42 +08:00

573 lines
24 KiB
JavaScript

#!/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