diff --git a/.changeset/add-headless-mode-create-expert.md b/.changeset/add-headless-mode-create-expert.md new file mode 100644 index 00000000..803abe97 --- /dev/null +++ b/.changeset/add-headless-mode-create-expert.md @@ -0,0 +1,6 @@ +--- +"create-expert": patch +"@perstack/tui": patch +--- + +Add --headless mode to create-expert for JSON output without TUI diff --git a/apps/create-expert/bin/cli.ts b/apps/create-expert/bin/cli.ts index 09c7a666..a37c26b7 100644 --- a/apps/create-expert/bin/cli.ts +++ b/apps/create-expert/bin/cli.ts @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs" import { PerstackError } from "@perstack/core" import { findLockfile, loadLockfile, parsePerstackConfig } from "@perstack/perstack-toml" -import { startHandler } from "@perstack/tui" +import { runHandler, startHandler } from "@perstack/tui" import { PROVIDER_ENV_MAP } from "@perstack/tui/provider-config" import { Command } from "commander" import packageJson from "../package.json" with { type: "json" } @@ -15,22 +15,69 @@ new Command() .description(packageJson.description) .version(packageJson.version) .argument("[query]", "Description of the expert to create or modify") + .option("--headless", "Run in headless mode with JSON output (no TUI)") + .option( + "--filter ", + "Filter events by type (comma-separated, e.g., completeRun,stopRunByError)", + ) + .option("--provider ", "Provider to use") + .option("--model ", "Model to use") + .option( + "--reasoning-budget ", + "Reasoning budget for native LLM reasoning (minimal, low, medium, high, or token count)", + ) + .option( + "--max-steps ", + "Maximum number of steps to run, default is undefined (no limit)", + ) + .option("--max-retries ", "Maximum number of generation retries, default is 5") + .option( + "--timeout ", + "Timeout for each generation in milliseconds, default is 300000 (5 minutes)", + ) + .option("--job-id ", "Job ID for identifying the job") + .option( + "--env-path ", + "Path to the environment file (can be specified multiple times), default is .env and .env.local", + (value: string, previous: string[]) => previous.concat(value), + [] as string[], + ) + .option("--verbose", "Enable verbose logging") .option("--continue", "Continue the most recent job with new query") .option("--continue-job ", "Continue the specified job with new query") + .option( + "--resume-from ", + "Resume from a specific checkpoint (requires --continue or --continue-job)", + ) + .option("-i, --interactive-tool-call-result", "Query is interactive tool call result") .action(async (query: string | undefined, options: Record) => { const config = parsePerstackConfig(readFileSync(tomlPath, "utf-8")) const lockfilePath = findLockfile() const lockfile = lockfilePath ? (loadLockfile(lockfilePath) ?? undefined) : undefined - await startHandler("expert", query, options, { - perstackConfig: config, - lockfile, - additionalEnv: (env) => { - const provider = config.provider?.providerName ?? "anthropic" - const envKey = PROVIDER_ENV_MAP[provider] - const value = envKey ? env[envKey] : undefined - return value ? { PROVIDER_API_KEY: value } : ({} as Record) - }, - }) + const additionalEnv = (env: Record) => { + const provider = config.provider?.providerName ?? "anthropic" + const envKey = PROVIDER_ENV_MAP[provider] + const value = envKey ? env[envKey] : undefined + return value ? { PROVIDER_API_KEY: value } : ({} as Record) + } + + if (options.headless) { + if (!query) { + console.error("Error: query argument is required in headless mode") + process.exit(1) + } + await runHandler("expert", query, options, { + perstackConfig: config, + lockfile, + additionalEnv, + }) + } else { + await startHandler("expert", query, options, { + perstackConfig: config, + lockfile, + additionalEnv, + }) + } }) .parseAsync() .catch((error) => { diff --git a/apps/create-expert/package.json b/apps/create-expert/package.json index 4dfc399d..4bd233d9 100644 --- a/apps/create-expert/package.json +++ b/apps/create-expert/package.json @@ -23,12 +23,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@perstack/core": "workspace:*", - "@perstack/perstack-toml": "workspace:*", - "@perstack/runtime": "workspace:*", "commander": "^14.0.3" }, "devDependencies": { + "@perstack/core": "workspace:*", + "@perstack/perstack-toml": "workspace:*", + "@perstack/runtime": "workspace:*", "@perstack/tui": "workspace:*", "@tsconfig/node22": "^22.0.5", "@types/node": "^25.2.3", diff --git a/packages/tui/src/run-handler.ts b/packages/tui/src/run-handler.ts index de8d57ca..dca57087 100644 --- a/packages/tui/src/run-handler.ts +++ b/packages/tui/src/run-handler.ts @@ -26,6 +26,7 @@ const defaultEventListener = (event: RunEvent | RuntimeEvent) => console.log(JSO export interface RunHandlerOptions { perstackConfig: PerstackConfig lockfile?: Lockfile + additionalEnv?: (env: Record) => Record } export async function runHandler( @@ -56,6 +57,10 @@ export async function runHandler( expertKey: input.expertKey, }) + if (handlerOptions?.additionalEnv) { + Object.assign(env, handlerOptions.additionalEnv(env)) + } + const lockfile = handlerOptions.lockfile // Generate job and run IDs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce7c6c22..1bea33b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,10 @@ importers: apps/create-expert: dependencies: + commander: + specifier: ^14.0.3 + version: 14.0.3 + devDependencies: '@perstack/core': specifier: workspace:* version: link:../../packages/core @@ -96,10 +100,6 @@ importers: '@perstack/runtime': specifier: workspace:* version: link:../../packages/runtime - commander: - specifier: ^14.0.3 - version: 14.0.3 - devDependencies: '@perstack/tui': specifier: workspace:* version: link:../../packages/tui