Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/thin-cli-package-extraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"perstack": patch
"create-expert": patch
"@perstack/create-expert-skill": patch
---

refactor: extract CLI business logic into @perstack/log and @perstack/installer packages, thin Commander interface
1 change: 1 addition & 0 deletions apps/create-expert-skill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@perstack/tui": "workspace:*",
"@tsconfig/node22": "^22.0.5",
"@types/node": "^25.2.3",
"tsup": "^8.5.1",
Expand Down
12 changes: 1 addition & 11 deletions apps/create-expert-skill/src/tools/run-expert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@ import { spawn } from "node:child_process"
import { existsSync } from "node:fs"
import path from "node:path"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { PROVIDER_ENV_MAP } from "@perstack/tui/provider-config"
import { dedent } from "ts-dedent"
import { z } from "zod/v4"
import { errorToolResult, successToolResult } from "../lib/tool-result.js"

const MAX_CONTENT_LENGTH = 500

const PROVIDER_ENV_MAP: Record<string, string | undefined> = {
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
openai: "OPENAI_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
"azure-openai": "AZURE_API_KEY",
"amazon-bedrock": "AWS_ACCESS_KEY_ID",
"google-vertex": "GOOGLE_APPLICATION_CREDENTIALS",
ollama: undefined,
}

function truncate(text: string, max = MAX_CONTENT_LENGTH): string {
if (text.length <= max) return text
return `${text.slice(0, max)}... [truncated]`
Expand Down
9 changes: 1 addition & 8 deletions apps/create-expert/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { readFileSync } from "node:fs"
import { parseWithFriendlyError, perstackConfigSchema } from "@perstack/core"
import { startHandler } from "@perstack/tui"
import { PROVIDER_ENV_MAP } from "@perstack/tui/provider-config"
import { Command } from "commander"
import TOML from "smol-toml"

Expand All @@ -12,14 +13,6 @@ const config = parseWithFriendlyError(
TOML.parse(readFileSync(tomlPath, "utf-8")),
)

const PROVIDER_ENV_MAP: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
openai: "OPENAI_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
"azure-openai": "AZURE_API_KEY",
}

new Command()
.name("create-expert")
.description("Create and modify Perstack expert definitions")
Expand Down
132 changes: 124 additions & 8 deletions apps/perstack/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,134 @@
#!/usr/bin/env node

import { installHandler } from "@perstack/installer"
import { logHandler, parsePositiveInt } from "@perstack/log"
import { runHandler, startHandler } from "@perstack/tui"
import { Command } from "commander"
import packageJson from "../package.json" with { type: "json" }
import { installCommand } from "../src/install.js"
import { logCommand } from "../src/log.js"
import { runCommand } from "../src/run.js"
import { startCommand } from "../src/start.js"

const program = new Command()
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version)
.addCommand(startCommand)
.addCommand(runCommand)
.addCommand(logCommand)
.addCommand(installCommand)

program
.command("start")
.description("Start Perstack with interactive TUI")
.argument("[expertKey]", "Expert key to run (optional, will prompt if not provided)")
.argument("[query]", "Query to run (optional, will prompt if not provided)")
.option("--config <configPath>", "Path to perstack.toml config file")
.option("--provider <provider>", "Provider to use")
.option("--model <model>", "Model to use")
.option(
"--reasoning-budget <budget>",
"Reasoning budget for native LLM reasoning (minimal, low, medium, high, or token count)",
)
.option(
"--max-steps <maxSteps>",
"Maximum number of steps to run, default is undefined (no limit)",
)
.option("--max-retries <maxRetries>", "Maximum number of generation retries, default is 5")
.option(
"--timeout <timeout>",
"Timeout for each generation in milliseconds, default is 300000 (5 minutes)",
)
.option("--job-id <jobId>", "Job ID for identifying the job")
.option(
"--env-path <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 <jobId>", "Continue the specified job with new query")
.option(
"--resume-from <checkpointId>",
"Resume from a specific checkpoint (requires --continue or --continue-job)",
)
.option("-i, --interactive-tool-call-result", "Query is interactive tool call result")
.action((expertKey, query, options) => startHandler(expertKey, query, options))

program
.command("run")
.description("Run Perstack with JSON output")
.argument("<expertKey>", "Expert key to run")
.argument("<query>", "Query to run")
.option("--config <configPath>", "Path to perstack.toml config file")
.option("--provider <provider>", "Provider to use")
.option("--model <model>", "Model to use")
.option(
"--reasoning-budget <budget>",
"Reasoning budget for native LLM reasoning (minimal, low, medium, high, or token count)",
)
.option(
"--max-steps <maxSteps>",
"Maximum number of steps to run, default is undefined (no limit)",
)
.option("--max-retries <maxRetries>", "Maximum number of generation retries, default is 5")
.option(
"--timeout <timeout>",
"Timeout for each generation in milliseconds, default is 300000 (5 minutes)",
)
.option("--job-id <jobId>", "Job ID for identifying the job")
.option(
"--env-path <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 <jobId>", "Continue the specified job with new query")
.option(
"--resume-from <checkpointId>",
"Resume from a specific checkpoint (requires --continue or --continue-job)",
)
.option("-i, --interactive-tool-call-result", "Query is interactive tool call result")
.option(
"--filter <types>",
"Filter events by type (comma-separated, e.g., completeRun,stopRunByError)",
)
.action((expertKey, query, options) => runHandler(expertKey, query, options))

program
.command("log")
.description("View execution history and events for debugging")
.option("--job <jobId>", "Show events for a specific job")
.option("--run <runId>", "Show events for a specific run")
.option("--checkpoint <checkpointId>", "Show checkpoint details")
.option("--step <step>", "Filter by step number (e.g., 5, >5, 1-10)")
.option("--type <type>", "Filter by event type")
.option("--errors", "Show error-related events only")
.option("--tools", "Show tool call events only")
.option("--delegations", "Show delegation events only")
.option("--filter <expression>", "Simple filter expression")
.option("--json", "Output as JSON")
.option("--pretty", "Pretty-print JSON output")
.option("--verbose", "Show full event details")
.option("--take <n>", "Number of events to display (default: 100, use 0 for all)", (val) =>
parsePositiveInt(val, "--take"),
)
.option("--offset <n>", "Number of events to skip (default: 0)", (val) =>
parsePositiveInt(val, "--offset"),
)
.option("--context <n>", "Include N events before/after matches", (val) =>
parsePositiveInt(val, "--context"),
)
.option("--messages", "Show message history for checkpoint")
.option("--summary", "Show summarized view")
.action((options) => logHandler(options))

program
.command("install")
.description("Generate perstack.lock with tool definitions for faster startup")
.option("--config <configPath>", "Path to perstack.toml config file")
.option(
"--env-path <path>",
"Path to the environment file (can be specified multiple times)",
(value: string, previous: string[]) => previous.concat(value),
[] as string[],
)
.action((options) => installHandler(options))

program.parse()
10 changes: 3 additions & 7 deletions apps/perstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@paralleldrive/cuid2": "^3.3.0",
"@perstack/api-client": "^0.0.55",
"@perstack/core": "workspace:*",
"@perstack/runtime": "workspace:*",
"commander": "^14.0.3",
"dotenv": "^17.3.1",
"ink": "^6.7.0",
"react": "^19.2.4",
"smol-toml": "^1.6.0"
"react": "^19.2.4"
},
"devDependencies": {
"@perstack/filesystem-storage": "workspace:*",
"@perstack/installer": "workspace:*",
"@perstack/log": "workspace:*",
"@perstack/tui": "workspace:*",
"@tsconfig/node22": "^22.0.5",
"@types/node": "^25.2.3",
Expand Down
87 changes: 0 additions & 87 deletions apps/perstack/src/log.test.ts

This file was deleted.

Loading