genaiscript
Version:
A CLI for GenAIScript, a generative AI scripting framework.
530 lines • 29.5 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/* eslint-disable n/no-process-exit */
/**
* CLI entry point for the GenAIScript tool, providing various commands and options
* for interacting with scripts, parsing files, testing, and managing cache.
*/
import { NODE_MIN_VERSION, PROMPTFOO_VERSION, NodeHost } from "@genaiscript/runtime";
import { Option, program } from "commander";
import { CORE_VERSION, DEBUG_SCRIPT_CATEGORY, GITHUB_REPO, MODEL_PROVIDERS, RUNTIME_ERROR_CODE, SERVER_PORT, TOOL_ID, TOOL_NAME, UNHANDLED_ERROR_CODE, GitClient, errorMessage, genaiscriptDebug, isQuiet, isRequestError, logPerformance, logVerbose, semverSatisfies, serializeError, setConsoleColors, setQuiet, } from "@genaiscript/core";
import { startServer } from "./server.js";
import { runScriptWithExitCode } from "./run.js";
import { retrievalFuzz, retrievalIndex, retrievalSearch } from "./retrieval.js";
import { helpAll } from "./help.js";
import { jsonl2json, parseAnyToJSON, parseDOCX, parseFence, parseHTMLToText, parseJinja2, parseMarkdown, parsePDF, parseSecrets, parseTokenize, parseTokens, prompty2genaiscript, } from "./parse.js";
import { createScript, fixScripts, listScripts, scriptInfo } from "./scripts.js";
import { envInfo, modelAliasesInfo, modelList, scriptModelInfo, systemInfo } from "./info.js";
import { scriptTestList, scriptTestsView, scriptsTest } from "./test.js";
import { cacheClear } from "./cache.js";
import "node:console";
import { convertFiles } from "./convert.js";
import { extractAudio, extractVideoFrames, probeVideo } from "./video.js";
import { configure } from "./configure.js";
import { listRuns } from "./runs.js";
import { startMcpServer } from "./mcpserver.js";
import { error } from "./log.js";
import { startOpenAPIServer } from "./openapi.js";
import { actionConfigure } from "./action.js";
import { resolve } from "node:path";
import debug from "debug";
import { githubActionConfigure } from "./githubaction.js";
import { uniq } from "es-toolkit";
import { compileScript } from "./typescript.js";
import { addRemoteOptions, applyRemoteOptions } from "./remote.js";
const dbg = genaiscriptDebug("cli");
export async function cli() {
let nodeHost; // Variable to hold NodeHost instance
// Handle uncaught exceptions globally
process.on("uncaughtException", (err) => {
const se = serializeError(err); // Serialize the error object
error(errorMessage(se)); // Log the error message
if (!isQuiet && se?.stack && nodeHost)
logVerbose(se?.stack); // Log stack trace if not in quiet mode
if (isRequestError(err)) {
const exitCode = err.status; // Use the error status as exit code
process.exit(exitCode); // Exit with the error status code
}
else
process.exit(UNHANDLED_ERROR_CODE); // Exit with a generic error code
});
// Verify Node.js version compatibility
if (!semverSatisfies(process.version, NODE_MIN_VERSION)) {
console.error(`node.js runtime incompatible, expected ${NODE_MIN_VERSION} got ${process.version}`);
process.exit(RUNTIME_ERROR_CODE); // Exit with runtime error code if version is incompatible
}
program.hook("preAction", async (cmd) => {
dbg(`opts: %O`, cmd.opts());
let { cwd } = cmd.opts();
const { env, include, githubWorkspace, remote, } = cmd.opts(); // Get environment options from command
const includes = []; // Array to hold include paths
let ignoreCurrentWorkspace = false;
if (include)
includes.push(resolve(include));
if (githubWorkspace) {
if (remote)
throw new Error("Cannot use --github-workspace with --remote");
const { workspaceDir } = githubActionConfigure();
if (workspaceDir && resolve(workspaceDir) !== resolve(process.cwd())) {
includes.push(resolve(process.cwd(), "genaisrc", "*.genai.mts"));
ignoreCurrentWorkspace = true;
cwd = resolve(workspaceDir);
dbg(`github action workspace: %s`, cwd);
GitClient.default().setGitHubWorkspace(cwd);
}
}
if (remote) {
// needed to run exec
NodeHost.install("", {
include: [],
});
// clone repo
const remoteDir = await applyRemoteOptions(cmd.opts());
if (!remoteDir)
throw new Error("Failed to configure remote repository");
includes.push(resolve(remoteDir, "**", "*.genai.*"));
ignoreCurrentWorkspace = true;
dbg(`remote workspace: %s`, remoteDir);
}
if (cwd) {
dbg(`chdir %s`, cwd);
process.chdir(cwd);
}
nodeHost = await NodeHost.install(env?.length ? env : undefined, {
include: includes.length
? uniq(includes).map((pattern) => ({
pattern,
ignoreGitIgnore: true,
}))
: undefined,
ignoreCurrentWorkspace,
}); // Install NodeHost with environment options
dbg(`cwd: %s`, process.cwd());
dbg(`config: %O`, nodeHost.config);
});
// Configure CLI program options and commands
program
.name(TOOL_ID)
.version(CORE_VERSION)
.description(`CLI for ${TOOL_NAME} ${GITHUB_REPO}`)
.showHelpAfterError(true)
.option("--cwd <string>", "Working directory")
.option("--include <string>", "Add 'include' directory to lookup scripts")
.option("--env <paths...>", "paths to .env files, defaults to './.env' if not specified")
.option("--no-colors", "disable color output")
.option("-q, --quiet", "disable verbose output")
.option("--perf", "enable performance logging")
.option("--github-workspace", "Use GitHub Actions workspace directory as cwd");
addRemoteOptions(program); // Add remote options to the program
program.on("option:no-colors", () => setConsoleColors(false));
program.on("option:quiet", () => setQuiet(true));
program.on("option:perf", () => logPerformance());
program.on("option:debug", (c) => debug.enable(c === DEBUG_SCRIPT_CATEGORY ? c : `genaiscript:${c}`));
const configureCmd = program.command("configure").description("Configure LLMs or GitHub Actions");
configureCmd.command("llm", { isDefault: true }).description("Configure LLM providers");
addProviderOptions(configureCmd).action(configure);
// Define 'run' command for executing scripts
const run = program
.command("run")
.description("Runs a GenAIScript against files.")
.arguments("<script> [files...]")
.option("--accept <string>", "comma separated list of accepted file extensions");
addModelOptions(run); // Add model options to the command
addLogProbsOptions(run)
.option("-e, --excluded-files <string...>", "excluded files")
.option("--ignore-git-ignore", "by default, files ignored by .gitignore are excluded. disables this mode")
.option("--fallback-tools", "Enable prompt-based tools instead of builtin LLM tool calling builtin tool calls")
.option("--mcps <string>", "path to MCP configuration file to override the script's MCP list")
.option("-o, --out <string>", "output folder. Extra markdown fields for output and trace will also be generated")
.option("--remove-out", "remove output folder if it exists")
.option("--out-trace <string>", "output file for trace")
.option("--out-output <string>", "output file for output")
.option("--out-data <string>", "output file for data (.jsonl/ndjson will be aggregated). JSON schema information and validation will be included if available.")
.option("--out-annotations <string>", "output file for annotations (.csv will be rendered as csv, .jsonl/ndjson will be aggregated)")
.option("--out-changelog <string>", "output file for changelogs");
addPullRequestOptions(run)
.option("--issue", "create a GitHub issue with the generation output")
.option("--teams-message", "Posts a message to the teams channel")
.option("-j, --json", "emit full JSON response to output")
.option(`--fail-on-errors`, `fails on detected annotation error`)
.option("--retry <number>", "number of retries")
.option("--retry-delay <number>", "minimum delay between retries")
.option("--max-delay <number>", "maximum delay between retries")
.option("--max-retry-after <number>", "maximum retry-after delay in milliseconds before giving up")
.option("-l, --label <string>", "label for the run")
.option("-t, --temperature <number>", "temperature for the run")
.option("--top-p <number>", "top-p for the run")
.option("--max-tokens <number>", "maximum completion tokens for the run")
.option("--max-data-repairs <number>", "maximum data repairs")
.option("--max-tool-calls <number>", "maximum tool calls for the run")
.option("--tool-choice <string>", "tool choice for the run, 'none', 'auto', 'required', or a function name")
.option("--seed <number>", "seed for the run")
.option("-c, --cache", "enable LLM result cache")
.option("--cache-name <name>", "custom cache file name")
.option("--csv-separator <string>", "csv separator", "\t")
.addOption(new Option("--fence-format <string>", "fence format").choices(["xml", "markdown", "none"]))
.option("-y, --apply-edits", "apply file edits")
.option("-x, --vars <namevalue...>", "variables, as name=value, stored in env.vars. Use environment variables GENAISCRIPT_VAR_name=value to pass variable through the environment")
.option("--run-retry <number>", "number of retries for the entire run")
.option("--no-run-trace", "disable automatic trace generation")
.option("--no-output-trace", "disable automatic output generation")
.option("--mcp-config <file>", "MCP configuration file (Claude format) to load servers from")
.action(runScriptWithExitCode); // Action to execute the script with exit code
// runs commands
const runs = program.command("runs").description("Commands to open previous runs");
runs
.command("list")
.alias("ls")
.description("List all available run reports in workspace")
.argument("[script]", "Script id")
.action(listRuns);
// Define 'test' command group for running tests
const test = program.command("test").alias("eval");
const testRun = test
.command("run", { isDefault: true })
.description("Runs the tests for scripts")
.argument("[script...]", "Script ids. If not provided, all scripts are tested")
.option("--redteam", "run red team tests");
addModelOptions(testRun) // Add model options to the command
.option("--models <models...>", "models to test where mode is the key value pair list of m (model), s (small model), t (temperature), p (top-p)")
.option("--max-concurrency <number>", "maximum concurrency", "1")
.option("-o, --out <folder>", "output folder")
.option("--remove-out", "remove output folder if it exists")
.option("--cli <string>", "override path to the cli")
.option("--test-delay <string>", "delay between tests in seconds")
.option("--cache", "enable LLM result cache")
.option("-r, --random", "Randomize test order")
.option("-v, --verbose", "verbose output")
.option("--promptfoo-version [version]", `promptfoo version, default is ${PROMPTFOO_VERSION}`)
.option("--out-summary <file>", "append output summary in file");
addGroupsOptions(testRun)
.option("--test-timeout <number>", "test timeout in seconds")
.option("--filter-model <string>", "filter scripts by model specified in script() function")
.action(scriptsTest); // Action to run the tests
// List available tests
const testList = test
.command("list")
.alias("ls")
.description("List available tests in workspace")
.option("--redteam", "list red team tests");
addGroupsOptions(testList)
.option("--filter-model <string>", "filter scripts by model specified in script() function")
.action(scriptTestList); // Action to list the tests
// Launch test viewer
test.command("view").description("Launch test viewer").action(scriptTestsView); // Action to view the tests
const convert = program
.command("convert")
.description("Converts file through a GenAIScript. Each file is processed separately through the GenAIScript and the LLM output is saved to a <filename>.genai.md (or custom suffix).")
.arguments("<script> [files...]")
.option("-u, --suffix <string>", "suffix for converted files")
.option("-r, --rewrite", "rewrite input file with output (overrides suffix)")
.option("-w, --cancel-word <string>", "cancel word which allows the LLM to notify to ignore output")
.option("-e, --excluded-files <string...>", "excluded files")
.option("--ignore-git-ignore", "by default, files ignored by .gitignore are excluded. disables this mode");
addModelOptions(convert)
.option("--fallback-tools", "Enable prompt-based tools instead of builtin LLM tool calling builtin tool calls")
.option("-o, --out <string>", "output folder. Extra markdown fields for output and trace will also be generated")
.option("-x, --vars <namevalue...>", "variables, as name=value, stored in env.vars. Use environment variables GENAISCRIPT_VAR_name=value to pass variable through the environment")
.option("-c, --cache", "enable LLM result cache")
.option("--cache-name <name>", "custom cache file name")
.option("--concurrency <number>", "number of concurrent conversions")
.option("--no-run-trace", "disable automatic trace generation")
.option("--no-output-trace", "disable automatic output generation")
.action(convertFiles);
// Define 'scripts' command group for script management tasks
const scripts = program
.command("scripts")
.alias("script")
.description("Utility tasks for scripts");
const scriptList = scripts
.command("list", { isDefault: true })
.alias("ls")
.description("List all available scripts in workspace")
.argument("[script...]", "Script ids")
.option("--unlisted", "show unlisted scripts")
.option("--json", "output in JSON format");
addGroupsOptions(scriptList).action(listScripts); // Action to list scripts
scripts
.command("create")
.description("Create a new script")
.argument("[name]", "Name of the script")
.option("-t, --typescript", "Generate TypeScript file (.genai.mts)", true)
.action(createScript); // Action to create a script
scripts
.command("fix")
.description("Write TypeScript definition files in the script folder to enable type checking.")
.option("--github-copilot-instructions", "Write GitHub Copilot custom instructions for better GenAIScript code generation")
.option("--docs", "Download documentation")
.option("--force", "Fix all folders, including built-in system scripts")
.action(fixScripts); // Action to fix scripts
scripts
.command("compile")
.description("Compile all scripts in workspace")
.argument("[folders...]", "Pattern to match files")
.action(compileScript); // Action to compile scripts
scripts
.command("model")
.description("List model connection information for scripts")
.argument("[script]", "Script id or file")
.option("-t, --token", "show token")
.action(scriptModelInfo); // Action to show model information
scripts
.command("help")
.alias("info")
.description("Show help information for a script")
.argument("<script>", "Script id")
.action(scriptInfo); // Action to show model information
// Define 'cache' command for cache management
const cache = program.command("cache").description("Cache management");
cache
.command("clear")
.description("Clear cache")
.argument("[name]", "Name of the cache, tests")
.action(cacheClear); // Action to clear cache
const video = program.command("video").description("Video tasks");
video
.command("probe")
.description("Probes metadata from a video/audio file")
.argument("<file>", "Audio or video file to inspect")
.action(probeVideo);
video
.command("extract-audio")
.description("Transcode video/audio file")
.argument("<file>", "Audio or video file to transcode")
.option("-t, --transcription", "Convert audio for speech-to-text")
.action(extractAudio);
video
.command("extract-frames")
.description("Extract video frames")
.argument("<file>", "Audio or video file to transcode")
.option("-k, --keyframes", "Extract only keyframes (intra frames)")
.option("-t, --scene-threshold <number>", "Extract frames with a minimum threshold")
.option("-c, --count <number>", "maximum number of frames to extract")
.option("-s, --size <string>", "size of the output frames wxh")
.option("-f, --format <string>", "Image file format")
.action(extractVideoFrames);
// Define 'retrieval' command group for RAG support
const retrieval = program.command("retrieval").description("RAG support");
retrieval
.command("index")
.arguments("<name> <files...>")
.description("Index files for vector search")
.option("-e, --excluded-files <string...>", "excluded files")
.option("--ignore-git-ignore", "by default, files ignored by .gitignore are excluded. disables this mode")
.option("-g, --embeddings-model <string>", "'embeddings' alias model")
.addOption(new Option("--database <string>", "Type of database to use for indexing").choices([
"local",
"azure_ai_search",
]))
.action(retrievalIndex); // Action to index files for vector search
retrieval
.command("vector")
.alias("search")
.description("Search using vector embeddings similarity")
.arguments("<query> [files...]")
.option("-e, --excluded-files <string...>", "excluded files")
.option("-k, --top-k <number>", "maximum number of results")
.option("-s, --min-score <number>", "minimum score")
.action(retrievalSearch); // Action to perform vector search
retrieval
.command("fuzz")
.description("Search using string distance")
.arguments("<query> [files...]")
.option("-e, --excluded-files <string...>", "excluded files")
.option("-k, --top-k <number>", "maximum number of results")
.option("-s, --min-score <number>", "minimum score")
.action(retrievalFuzz); // Action to perform fuzzy search
// Define 'serve' command to start a local server
const serve = program
.command("serve")
.description("Start a GenAIScript local web server")
.option("--port <number>", `Specify the port number, default: ${SERVER_PORT}`)
.option("--api-key <string>", "API key to authenticate requests")
.option("--network", "Opens server on 0.0.0.0 to make it accessible on the network")
.option("--cors <string>", "Enable CORS and sets the allowed origin. Use '*' to allow any origin.")
.option("--chat", "Enable OpenAI compatible chat completion routes (/v1/chat/completions)")
.option("--dispatch-progress", "Dispatch progress events to all clients")
.option("--github-copilot-chat-client", "Allow github_copilot_chat provider to connect to connected Visual Studio Code")
.option("--no-run-trace", "Emit run trace events")
.action(startServer); // Action to start the server
addModelOptions(serve);
const mcp = program.command("mcp").option("--ids <string...>", "Filter script by ids");
addGroupsOptions(mcp)
.option("--startup <string>", "Startup script id, executed after the server is started")
.option("--http", "Use HTTP transport instead of stdio")
.option("--port <number>", `HTTP port number, default: ${SERVER_PORT}`)
.option("-n, --network", "Opens HTTP server on 0.0.0.0 to make it accessible on the network")
.alias("mcps")
.description("Starts a Model Context Protocol server that exposes scripts as tools. Use --http for HTTP transport.")
.action(startMcpServer);
addModelOptions(mcp);
const openapi = program
.command("webapi")
.alias("openapi")
.option("-n, --network", "Opens server on 0.0.0.0 to make it accessible on the network")
.option("--port <number>", `Specify the port number, default: ${SERVER_PORT}`)
.option("--cors <string>", "Enable CORS and sets the allowed origin. Use '*' to allow any origin.")
.option("--route <string>", "Route prefix, like /api")
.option("--ids <string...>", "Filter script by ids")
.option("--startup <string>", "Startup script id, executed after the server is started")
.description("Starts an Web API server that exposes scripts as REST endpoints (OpenAPI 3.1 compatible)")
.action(startOpenAPIServer);
addModelOptions(openapi);
addGroupsOptions(openapi);
const configureActionCmd = configureCmd
.command("action")
.alias("github-action")
.description("Configure a GitHub repository as a custom dockerized GitHub Action")
.argument("[script]", "Script id to use as action", "action")
.option("-f, --force", "force override existing action files")
.option("-o, --out <string>", "output folder for action files")
.option("--ffmpeg", "use ffmpeg for video/audio processing")
.option("--playwright", "Enable Playwright for browser testing")
.option("--python", "Install Python 3.x support")
.option("-i, --image <string>", "Docker image identifier")
.option("--apks <string...>", "Linux packages to install")
.option("--provider <string>", "LLM provider to use")
.option("--interactive", "Enable interactive mode")
.action(actionConfigure);
configureActionCmd.addOption(new Option("-e, --event <string>", "GitHub event type").choices([
"push",
"pull_request",
"issue_comment",
"issue",
]));
addPullRequestOptions(configureActionCmd); // Add pull request options to the action command
// Define 'parse' command group for parsing tasks
const parser = program.command("parse").alias("parsers").description("Parse various outputs");
const parserData = parser
.command("data <file>")
.description("Convert CSV, YAML, TOML, INI, XLSX, XML, MD/X frontmatter or JSON data files into various formats")
.action(parseAnyToJSON);
parserData.addOption(new Option("-f, --format <string>", "output format").choices([
"json",
"json5",
"yaml",
"ini",
"csv",
"md",
]));
parser
.command("fence <language> <file>")
.description("Extracts a code fenced regions of the given type")
.action(parseFence); // Action to parse fenced code regions
parser
.command("pdf <file>")
.description("Parse a PDF into text and images")
.option("-i, --images", "extract images")
.option("-o, --out <string>", "output folder")
.action(parsePDF); // Action to parse PDF files
parser
.command("docx <file>")
.description("Parse a DOCX into texts")
.addOption(new Option("-f, --format <string>", "output format").choices(["markdown", "html", "text"]))
.action(parseDOCX); // Action to parse DOCX files
parser
.command("html")
.argument("<file_or_url>", "HTML file or URL")
.addOption(new Option("-f, --format <string>", "output format").choices(["markdown", "text"]))
.option("-o, --out <string>", "output file")
.description("Parse an HTML file to text")
.action(parseHTMLToText); // Action to parse HTML files
parser
.command("tokens")
.description("Count tokens in a set of files")
.arguments("<files...>")
.option("-e, --excluded-files <string...>", "excluded files")
.action(parseTokens); // Action to count tokens in files
parser
.command("tokenize")
.argument("<file>", "file to tokenize")
.description("Tokenizes a piece of text and display the tokens (in hex format)")
.option("-m, --model <string>", "encoding model")
.action(parseTokenize);
parser
.command("jsonl2json", "Converts JSONL files to a JSON file")
.argument("<file...>", "input JSONL files")
.action(jsonl2json); // Action to convert JSONL to JSON
parser
.command("prompty")
.description("Converts .prompty files to genaiscript")
.argument("<file...>", "input JSONL files")
.option("-o, --out <string>", "output folder")
.action(prompty2genaiscript); // Action to convert prompty files
parser
.command("jinja2")
.description("Renders Jinja2 or prompty template")
.argument("<file>", "input Jinja2 or prompty template file")
.option("-x, --vars <namevalue...>", "variables, as name=value passed to the template")
.action(parseJinja2);
parser
.command("secrets")
.description("Applies secret scanning and redaction to files")
.argument("<file...>", "input files")
.action(parseSecrets);
parser
.command("markdown")
.description("Chunks markdown files")
.argument("<file>", "input markdown file")
.option("-m, --model <string>", "encoding model")
.option("--max-tokens <number>", "maximum tokens per chunk")
.action(parseMarkdown);
// Define 'info' command group for utility information tasks
const info = program.command("info").description("Utility tasks");
info.command("help").description("Show help for all commands").action(helpAll); // Action to show help for commands
info.command("system").description("Show system information").action(systemInfo); // Action to show system information
info
.command("env")
.description("Show .env information")
.arguments("[provider]")
.option("-t, --token", "show token")
.option("-e, --error", "show errors")
.option("-m, --models", "show models if possible")
.action(envInfo); // Action to show environment information
const models = program.command("models");
const modelsList = models
.command("list", { isDefault: true })
.alias("ls")
.description("List all available models")
.arguments("[provider]");
modelsList
.addOption(new Option("-f, --format <string>", "output format").choices(["json", "yaml"]))
.action(modelList);
models
.command("alias")
.option("--check", "Check model aliases configuration by running an inference test")
.description("Show model alias mapping")
.action(modelAliasesInfo);
program.parse(); // Parse command-line arguments
function addGroupsOptions(command) {
return command.option("-g, --groups <groups...>", "groups to include or exclude. Use :! prefix to exclude");
}
function addPullRequestOptions(command) {
return command
.option("-n, --pull-request-comment [string]", "create comment on a pull request with a unique id (defaults to script id)")
.option("-d, --pull-request-description [string]", "create comment on a pull request description with a unique id (defaults to script id)")
.option("-r, --pull-request-reviews", "create pull request reviews from annotations");
}
function addLogProbsOptions(command) {
return command
.option("--logprobs", "enable reporting token probabilities")
.option("--top-logprobs <number>", "number of top logprobs (1 to 5)");
}
function addProviderOptions(command) {
return command.addOption(new Option("-p, --provider <string>", "Preferred LLM provider aliases").choices(MODEL_PROVIDERS.filter(({ hidden }) => !hidden).map(({ id }) => id)));
}
function addModelOptions(command) {
return addProviderOptions(command)
.option("-m, --model <string>", "'large' model alias (default)")
.option("-s, --small-model <string>", "'small' alias model")
.option("--vision-model <string>", "'vision' alias model")
.option("--embeddings-model <string>", "'embeddings' alias model")
.option("-a, --model-alias <nameid...>", "model alias as name=modelid")
.addOption(new Option("--reasoning-effort <string>", "Reasoning effort for o* models").choices([
"high",
"medium",
"low",
]));
}
}
//# sourceMappingURL=cli.js.map