From 86f72f4b3d8995c311a2a970f81ffbd45a1d5131 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Sun, 15 Feb 2026 03:38:36 +0000 Subject: [PATCH 1/2] refactor: extract CLI business logic into packages, thin Commander interface - Create @perstack/log package from apps/perstack/src/lib/log/ and src/log.ts - Create @perstack/installer package from apps/perstack/src/install.ts - Extend @perstack/tui with runHandler, PROVIDER_ENV_MAP, findConfigPath - Rewrite apps/perstack/bin/cli.ts as declarative Commander definitions - Remove apps/perstack/src/ directory entirely - Deduplicate PROVIDER_ENV_MAP across create-expert and create-expert-skill Co-Authored-By: Claude Opus 4.6 --- apps/create-expert-skill/package.json | 1 + .../src/tools/run-expert.ts | 12 +- apps/create-expert/bin/cli.ts | 9 +- apps/perstack/bin/cli.ts | 132 ++++++++++++++- apps/perstack/package.json | 10 +- apps/perstack/src/log.test.ts | 87 ---------- apps/perstack/src/run.ts | 155 ------------------ apps/perstack/src/start.ts | 40 ----- packages/installer/package.json | 46 ++++++ .../installer/src/expert-resolver.ts | 115 +------------ packages/installer/src/handler.ts | 53 ++++++ packages/installer/src/index.ts | 8 + packages/installer/src/lockfile-generator.ts | 28 ++++ packages/installer/tsconfig.json | 5 + packages/log/package.json | 43 +++++ .../log/src}/data-fetcher.test.ts | 0 .../log => packages/log/src}/data-fetcher.ts | 1 - .../log => packages/log/src}/filter.test.ts | 0 .../lib/log => packages/log/src}/filter.ts | 0 .../log/src}/formatter.test.ts | 0 .../lib/log => packages/log/src}/formatter.ts | 6 +- .../src/log.ts => packages/log/src/handler.ts | 81 +++------ .../src/lib/log => packages/log/src}/index.ts | 1 + .../src/lib/log => packages/log/src}/types.ts | 0 packages/log/tsconfig.json | 5 + packages/tui/package.json | 3 +- packages/tui/src/index.ts | 1 + packages/tui/src/lib/perstack-toml.ts | 20 +++ packages/tui/src/lib/provider-config.ts | 11 ++ packages/tui/src/run-handler.ts | 118 +++++++++++++ packages/tui/tsup.config.ts | 1 + pnpm-lock.yaml | 84 ++++++++-- 32 files changed, 572 insertions(+), 504 deletions(-) delete mode 100644 apps/perstack/src/log.test.ts delete mode 100644 apps/perstack/src/run.ts delete mode 100644 apps/perstack/src/start.ts create mode 100644 packages/installer/package.json rename apps/perstack/src/install.ts => packages/installer/src/expert-resolver.ts (58%) create mode 100644 packages/installer/src/handler.ts create mode 100644 packages/installer/src/index.ts create mode 100644 packages/installer/src/lockfile-generator.ts create mode 100644 packages/installer/tsconfig.json create mode 100644 packages/log/package.json rename {apps/perstack/src/lib/log => packages/log/src}/data-fetcher.test.ts (100%) rename {apps/perstack/src/lib/log => packages/log/src}/data-fetcher.ts (99%) rename {apps/perstack/src/lib/log => packages/log/src}/filter.test.ts (100%) rename {apps/perstack/src/lib/log => packages/log/src}/filter.ts (100%) rename {apps/perstack/src/lib/log => packages/log/src}/formatter.test.ts (100%) rename {apps/perstack/src/lib/log => packages/log/src}/formatter.ts (98%) rename apps/perstack/src/log.ts => packages/log/src/handler.ts (69%) rename {apps/perstack/src/lib/log => packages/log/src}/index.ts (89%) rename {apps/perstack/src/lib/log => packages/log/src}/types.ts (100%) create mode 100644 packages/log/tsconfig.json create mode 100644 packages/tui/src/run-handler.ts diff --git a/apps/create-expert-skill/package.json b/apps/create-expert-skill/package.json index c42d07cf..afb8f6c6 100644 --- a/apps/create-expert-skill/package.json +++ b/apps/create-expert-skill/package.json @@ -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", diff --git a/apps/create-expert-skill/src/tools/run-expert.ts b/apps/create-expert-skill/src/tools/run-expert.ts index ca34a74d..a0e20ce2 100644 --- a/apps/create-expert-skill/src/tools/run-expert.ts +++ b/apps/create-expert-skill/src/tools/run-expert.ts @@ -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 = { - 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]` diff --git a/apps/create-expert/bin/cli.ts b/apps/create-expert/bin/cli.ts index e76e5bc6..e6a3bc25 100644 --- a/apps/create-expert/bin/cli.ts +++ b/apps/create-expert/bin/cli.ts @@ -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" @@ -12,14 +13,6 @@ const config = parseWithFriendlyError( TOML.parse(readFileSync(tomlPath, "utf-8")), ) -const PROVIDER_ENV_MAP: Record = { - 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") diff --git a/apps/perstack/bin/cli.ts b/apps/perstack/bin/cli.ts index 9ccd7f76..a322a496 100755 --- a/apps/perstack/bin/cli.ts +++ b/apps/perstack/bin/cli.ts @@ -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 ", "Path to perstack.toml config file") + .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((expertKey, query, options) => startHandler(expertKey, query, options)) + +program + .command("run") + .description("Run Perstack with JSON output") + .argument("", "Expert key to run") + .argument("", "Query to run") + .option("--config ", "Path to perstack.toml config file") + .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") + .option( + "--filter ", + "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 ", "Show events for a specific job") + .option("--run ", "Show events for a specific run") + .option("--checkpoint ", "Show checkpoint details") + .option("--step ", "Filter by step number (e.g., 5, >5, 1-10)") + .option("--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 ", "Simple filter expression") + .option("--json", "Output as JSON") + .option("--pretty", "Pretty-print JSON output") + .option("--verbose", "Show full event details") + .option("--take ", "Number of events to display (default: 100, use 0 for all)", (val) => + parsePositiveInt(val, "--take"), + ) + .option("--offset ", "Number of events to skip (default: 0)", (val) => + parsePositiveInt(val, "--offset"), + ) + .option("--context ", "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 ", "Path to perstack.toml config file") + .option( + "--env-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() diff --git a/apps/perstack/package.json b/apps/perstack/package.json index 18ce2af1..45a0c46d 100644 --- a/apps/perstack/package.json +++ b/apps/perstack/package.json @@ -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", diff --git a/apps/perstack/src/log.test.ts b/apps/perstack/src/log.test.ts deleted file mode 100644 index c67fa047..00000000 --- a/apps/perstack/src/log.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" - -vi.mock("@perstack/filesystem-storage", () => ({ - getAllJobs: vi.fn(), - retrieveJob: vi.fn(), - getCheckpointsByJobId: vi.fn(), - defaultRetrieveCheckpoint: vi.fn(), - getEventContents: vi.fn(), - getAllRuns: vi.fn(), - getRunIdsByJobId: vi.fn(), -})) - -describe("logCommand", () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it("exports logCommand", async () => { - const { logCommand } = await import("./log.js") - expect(logCommand).toBeDefined() - expect(logCommand.name()).toBe("log") - }) - - it("has required options", async () => { - const { logCommand } = await import("./log.js") - const options = logCommand.options.map((o) => o.long) - expect(options).toContain("--job") - expect(options).toContain("--run") - expect(options).toContain("--checkpoint") - expect(options).toContain("--step") - expect(options).toContain("--type") - expect(options).toContain("--errors") - expect(options).toContain("--tools") - expect(options).toContain("--delegations") - expect(options).toContain("--filter") - expect(options).toContain("--json") - expect(options).toContain("--pretty") - expect(options).toContain("--verbose") - expect(options).toContain("--take") - expect(options).toContain("--offset") - expect(options).toContain("--context") - expect(options).toContain("--messages") - expect(options).toContain("--summary") - }) - - it("has correct description", async () => { - const { logCommand } = await import("./log.js") - expect(logCommand.description()).toBe("View execution history and events for debugging") - }) - - it("validates --take option rejects non-numeric values", async () => { - const { logCommand } = await import("./log.js") - const takeOption = logCommand.options.find((o) => o.long === "--take") - expect(takeOption).toBeDefined() - // The parseArg function should throw for non-numeric values - expect(() => takeOption!.parseArg!("abc", undefined)).toThrow( - 'Invalid value for --take: "abc" is not a valid number', - ) - }) - - it("validates --take option rejects negative values", async () => { - const { logCommand } = await import("./log.js") - const takeOption = logCommand.options.find((o) => o.long === "--take") - expect(takeOption).toBeDefined() - expect(() => takeOption!.parseArg!("-5", undefined)).toThrow( - 'Invalid value for --take: "-5" must be non-negative', - ) - }) - - it("validates --offset option rejects non-numeric values", async () => { - const { logCommand } = await import("./log.js") - const offsetOption = logCommand.options.find((o) => o.long === "--offset") - expect(offsetOption).toBeDefined() - expect(() => offsetOption!.parseArg!("xyz", undefined)).toThrow( - 'Invalid value for --offset: "xyz" is not a valid number', - ) - }) - - it("validates --context option rejects non-numeric values", async () => { - const { logCommand } = await import("./log.js") - const contextOption = logCommand.options.find((o) => o.long === "--context") - expect(contextOption).toBeDefined() - expect(() => contextOption!.parseArg!("foo", undefined)).toThrow( - 'Invalid value for --context: "foo" is not a valid number', - ) - }) -}) diff --git a/apps/perstack/src/run.ts b/apps/perstack/src/run.ts deleted file mode 100644 index 0c4d06f4..00000000 --- a/apps/perstack/src/run.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { createId } from "@paralleldrive/cuid2" -import type { RunEvent, RuntimeEvent } from "@perstack/core" -import { - createFilteredEventListener, - parseWithFriendlyError, - runCommandInputSchema, - validateEventFilter, -} from "@perstack/core" -import { - createInitialJob, - defaultRetrieveCheckpoint, - defaultStoreCheckpoint, - defaultStoreEvent, - retrieveJob, - storeJob, -} from "@perstack/filesystem-storage" -import { findLockfile, loadLockfile, run as perstackRun } from "@perstack/runtime" -import { resolveRunContext } from "@perstack/tui/context" -import { - parseInteractiveToolCallResult, - parseInteractiveToolCallResultJson, -} from "@perstack/tui/interactive" -import { Command } from "commander" - -const defaultEventListener = (event: RunEvent | RuntimeEvent) => console.log(JSON.stringify(event)) - -export const runCommand = new Command() - .command("run") - .description("Run Perstack with JSON output") - .argument("", "Expert key to run") - .argument("", "Query to run") - .option("--config ", "Path to perstack.toml config file") - .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") - .option( - "--filter ", - "Filter events by type (comma-separated, e.g., completeRun,stopRunByError)", - ) - .action(async (expertKey, query, options) => { - const input = parseWithFriendlyError(runCommandInputSchema, { expertKey, query, options }) - - // Validate and apply event filter if specified - let eventListener = defaultEventListener - if (input.options.filter && input.options.filter.length > 0) { - try { - const validatedTypes = validateEventFilter(input.options.filter) - const allowedTypes = new Set(validatedTypes) - eventListener = createFilteredEventListener(defaultEventListener, allowedTypes) - } catch (error) { - if (error instanceof Error) { - console.error(error.message) - } else { - console.error(error) - } - process.exit(1) - } - } - - try { - const { perstackConfig, checkpoint, env, providerConfig, model, experts } = - await resolveRunContext({ - configPath: input.options.config, - provider: input.options.provider, - model: input.options.model, - envPath: input.options.envPath, - continue: input.options.continue, - continueJob: input.options.continueJob, - resumeFrom: input.options.resumeFrom, - expertKey: input.expertKey, - }) - - // Load lockfile if present - const lockfilePath = findLockfile() - const lockfile = lockfilePath ? (loadLockfile(lockfilePath) ?? undefined) : undefined - - // Generate job and run IDs - const jobId = checkpoint?.jobId ?? input.options.jobId ?? createId() - const runId = createId() - - await perstackRun( - { - setting: { - jobId, - runId, - expertKey: input.expertKey, - input: input.options.interactiveToolCallResult - ? (parseInteractiveToolCallResultJson(input.query) ?? - (checkpoint - ? parseInteractiveToolCallResult(input.query, checkpoint) - : { text: input.query })) - : { text: input.query }, - experts, - model, - providerConfig, - reasoningBudget: input.options.reasoningBudget ?? perstackConfig.reasoningBudget, - maxSteps: input.options.maxSteps ?? perstackConfig.maxSteps, - maxRetries: input.options.maxRetries ?? perstackConfig.maxRetries, - timeout: input.options.timeout ?? perstackConfig.timeout, - perstackApiBaseUrl: perstackConfig.perstackApiBaseUrl, - perstackApiKey: env.PERSTACK_API_KEY, - perstackBaseSkillCommand: perstackConfig.perstackBaseSkillCommand, - env, - proxyUrl: process.env.PERSTACK_PROXY_URL, - verbose: input.options.verbose, - }, - checkpoint, - }, - { - eventListener, - storeCheckpoint: defaultStoreCheckpoint, - storeEvent: defaultStoreEvent, - retrieveCheckpoint: defaultRetrieveCheckpoint, - storeJob, - retrieveJob, - createJob: createInitialJob, - lockfile, - }, - ) - } catch (error) { - if (error instanceof Error) { - console.error(error.message) - } else { - console.error(error) - } - process.exit(1) - } - }) diff --git a/apps/perstack/src/start.ts b/apps/perstack/src/start.ts deleted file mode 100644 index b77383ca..00000000 --- a/apps/perstack/src/start.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { startHandler } from "@perstack/tui" -import { Command } from "commander" - -export const startCommand = new Command() - .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 ", "Path to perstack.toml config file") - .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((expertKey, query, options) => startHandler(expertKey, query, options)) diff --git a/packages/installer/package.json b/packages/installer/package.json new file mode 100644 index 00000000..5d7a022d --- /dev/null +++ b/packages/installer/package.json @@ -0,0 +1,46 @@ +{ + "private": true, + "version": "0.0.1", + "name": "@perstack/installer", + "description": "Perstack Installer - Generate lockfiles with tool definitions", + "author": "Wintermute Technologies, Inc.", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + } + }, + "files": [ + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "build": "pnpm run clean && tsup --config ../../tsup.config.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@perstack/api-client": "^0.0.55", + "@perstack/core": "workspace:*", + "@perstack/runtime": "workspace:*", + "smol-toml": "^1.6.0" + }, + "devDependencies": { + "@perstack/tui": "workspace:*", + "@tsconfig/node22": "^22.0.5", + "@types/node": "^25.2.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/apps/perstack/src/install.ts b/packages/installer/src/expert-resolver.ts similarity index 58% rename from apps/perstack/src/install.ts rename to packages/installer/src/expert-resolver.ts index 397af168..88adb126 100644 --- a/apps/perstack/src/install.ts +++ b/packages/installer/src/expert-resolver.ts @@ -1,43 +1,14 @@ -import { readFile, writeFile } from "node:fs/promises" -import path from "node:path" import { createApiClient } from "@perstack/api-client" import { defaultPerstackApiBaseUrl, type Expert, expertSchema, - type Lockfile, - type LockfileExpert, type PerstackConfig, type RuntimeVersion, type Skill, } from "@perstack/core" -import { collectToolDefinitionsForExpert } from "@perstack/runtime" -import { getEnv } from "@perstack/tui/get-env" -import { getPerstackConfig } from "@perstack/tui/perstack-toml" -import { Command } from "commander" -import TOML from "smol-toml" -async function findConfigPath(configPath?: string): Promise { - if (configPath) { - return path.resolve(process.cwd(), configPath) - } - return await findConfigPathRecursively(process.cwd()) -} - -async function findConfigPathRecursively(cwd: string): Promise { - const configPath = path.resolve(cwd, "perstack.toml") - try { - await readFile(configPath) - return configPath - } catch { - if (cwd === path.parse(cwd).root) { - throw new Error("perstack.toml not found. Create one or specify --config path.") - } - return await findConfigPathRecursively(path.dirname(cwd)) - } -} - -type PublishedExpertData = { +export type PublishedExpertData = { name: string version: string description?: string @@ -77,7 +48,7 @@ type PublishedExpertData = { tags?: string[] } -function toRuntimeExpert(key: string, expert: PublishedExpertData): Expert { +export function toRuntimeExpert(key: string, expert: PublishedExpertData): Expert { const skills: Record = Object.fromEntries( Object.entries(expert.skills ?? {}).map(([name, skill]) => { switch (skill.type) { @@ -150,7 +121,7 @@ function toRuntimeExpert(key: string, expert: PublishedExpertData): Expert { } } -function configExpertToExpert( +export function configExpertToExpert( key: string, configExpert: NonNullable[string], ): Expert { @@ -169,7 +140,7 @@ function configExpertToExpert( }) } -async function resolveAllExperts( +export async function resolveAllExperts( config: PerstackConfig, env: Record, ): Promise> { @@ -218,81 +189,3 @@ async function resolveAllExperts( } return experts } - -function expertToLockfileExpert( - expert: Expert, - toolDefinitions: { - skillName: string - name: string - description?: string - inputSchema: Record - }[], -): LockfileExpert { - return { - key: expert.key, - name: expert.name, - version: expert.version, - description: expert.description, - instruction: expert.instruction, - skills: expert.skills, - delegates: expert.delegates, - tags: expert.tags, - toolDefinitions, - } -} - -function generateLockfileToml(lockfile: Lockfile): string { - return TOML.stringify(lockfile) -} - -export const installCommand = new Command() - .command("install") - .description("Generate perstack.lock with tool definitions for faster startup") - .option("--config ", "Path to perstack.toml config file") - .option( - "--env-path ", - "Path to the environment file (can be specified multiple times)", - (value: string, previous: string[]) => previous.concat(value), - [] as string[], - ) - .action(async (options) => { - try { - const configPath = await findConfigPath(options.config) - const config = await getPerstackConfig(options.config) - const envPath = - options.envPath && options.envPath.length > 0 - ? options.envPath - : (config.envPath ?? [".env", ".env.local"]) - const env = getEnv(envPath) - console.log("Resolving experts...") - const experts = await resolveAllExperts(config, env) - console.log(`Found ${Object.keys(experts).length} expert(s)`) - const lockfileExperts: Record = {} - for (const [key, expert] of Object.entries(experts)) { - console.log(`Collecting tool definitions for ${key}...`) - const toolDefinitions = await collectToolDefinitionsForExpert(expert, { - env, - perstackBaseSkillCommand: config.perstackBaseSkillCommand, - }) - console.log(` Found ${toolDefinitions.length} tool(s)`) - lockfileExperts[key] = expertToLockfileExpert(expert, toolDefinitions) - } - const lockfile: Lockfile = { - version: "1", - generatedAt: Date.now(), - configPath: path.basename(configPath), - experts: lockfileExperts, - } - const lockfilePath = path.join(path.dirname(configPath), "perstack.lock") - const lockfileContent = generateLockfileToml(lockfile) - await writeFile(lockfilePath, lockfileContent, "utf-8") - console.log(`Generated ${lockfilePath}`) - } catch (error) { - if (error instanceof Error) { - console.error(`Error: ${error.message}`) - } else { - console.error(error) - } - process.exit(1) - } - }) diff --git a/packages/installer/src/handler.ts b/packages/installer/src/handler.ts new file mode 100644 index 00000000..3df0e817 --- /dev/null +++ b/packages/installer/src/handler.ts @@ -0,0 +1,53 @@ +import { writeFile } from "node:fs/promises" +import path from "node:path" +import type { Lockfile, LockfileExpert } from "@perstack/core" +import { collectToolDefinitionsForExpert } from "@perstack/runtime" +import { getEnv } from "@perstack/tui/get-env" +import { findConfigPath, getPerstackConfig } from "@perstack/tui/perstack-toml" +import { resolveAllExperts } from "./expert-resolver.js" +import { expertToLockfileExpert, generateLockfileToml } from "./lockfile-generator.js" + +export async function installHandler(options: { + config?: string + envPath?: string[] +}): Promise { + try { + const configPath = await findConfigPath(options.config) + const config = await getPerstackConfig(options.config) + const envPath = + options.envPath && options.envPath.length > 0 + ? options.envPath + : (config.envPath ?? [".env", ".env.local"]) + const env = getEnv(envPath) + console.log("Resolving experts...") + const experts = await resolveAllExperts(config, env) + console.log(`Found ${Object.keys(experts).length} expert(s)`) + const lockfileExperts: Record = {} + for (const [key, expert] of Object.entries(experts)) { + console.log(`Collecting tool definitions for ${key}...`) + const toolDefinitions = await collectToolDefinitionsForExpert(expert, { + env, + perstackBaseSkillCommand: config.perstackBaseSkillCommand, + }) + console.log(` Found ${toolDefinitions.length} tool(s)`) + lockfileExperts[key] = expertToLockfileExpert(expert, toolDefinitions) + } + const lockfile: Lockfile = { + version: "1", + generatedAt: Date.now(), + configPath: path.basename(configPath), + experts: lockfileExperts, + } + const lockfilePath = path.join(path.dirname(configPath), "perstack.lock") + const lockfileContent = generateLockfileToml(lockfile) + await writeFile(lockfilePath, lockfileContent, "utf-8") + console.log(`Generated ${lockfilePath}`) + } catch (error) { + if (error instanceof Error) { + console.error(`Error: ${error.message}`) + } else { + console.error(error) + } + process.exit(1) + } +} diff --git a/packages/installer/src/index.ts b/packages/installer/src/index.ts new file mode 100644 index 00000000..5c99514a --- /dev/null +++ b/packages/installer/src/index.ts @@ -0,0 +1,8 @@ +export { + configExpertToExpert, + type PublishedExpertData, + resolveAllExperts, + toRuntimeExpert, +} from "./expert-resolver.js" +export { installHandler } from "./handler.js" +export { expertToLockfileExpert, generateLockfileToml } from "./lockfile-generator.js" diff --git a/packages/installer/src/lockfile-generator.ts b/packages/installer/src/lockfile-generator.ts new file mode 100644 index 00000000..548e4247 --- /dev/null +++ b/packages/installer/src/lockfile-generator.ts @@ -0,0 +1,28 @@ +import type { Expert, Lockfile, LockfileExpert } from "@perstack/core" +import TOML from "smol-toml" + +export function expertToLockfileExpert( + expert: Expert, + toolDefinitions: { + skillName: string + name: string + description?: string + inputSchema: Record + }[], +): LockfileExpert { + return { + key: expert.key, + name: expert.name, + version: expert.version, + description: expert.description, + instruction: expert.instruction, + skills: expert.skills, + delegates: expert.delegates, + tags: expert.tags, + toolDefinitions, + } +} + +export function generateLockfileToml(lockfile: Lockfile): string { + return TOML.stringify(lockfile) +} diff --git a/packages/installer/tsconfig.json b/packages/installer/tsconfig.json new file mode 100644 index 00000000..a10de72d --- /dev/null +++ b/packages/installer/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/log/package.json b/packages/log/package.json new file mode 100644 index 00000000..f1eec1db --- /dev/null +++ b/packages/log/package.json @@ -0,0 +1,43 @@ +{ + "private": true, + "version": "0.0.1", + "name": "@perstack/log", + "description": "Perstack Log - Execution history viewer", + "author": "Wintermute Technologies, Inc.", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + } + }, + "files": [ + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "build": "pnpm run clean && tsup --config ../../tsup.config.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@perstack/core": "workspace:*", + "@perstack/filesystem-storage": "workspace:*" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.5", + "@types/node": "^25.2.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/apps/perstack/src/lib/log/data-fetcher.test.ts b/packages/log/src/data-fetcher.test.ts similarity index 100% rename from apps/perstack/src/lib/log/data-fetcher.test.ts rename to packages/log/src/data-fetcher.test.ts diff --git a/apps/perstack/src/lib/log/data-fetcher.ts b/packages/log/src/data-fetcher.ts similarity index 99% rename from apps/perstack/src/lib/log/data-fetcher.ts rename to packages/log/src/data-fetcher.ts index 5daa9a0e..26853c65 100644 --- a/apps/perstack/src/lib/log/data-fetcher.ts +++ b/packages/log/src/data-fetcher.ts @@ -7,7 +7,6 @@ import { getAllRuns, getCheckpointsByJobId, getEventContents, - getRunIdsByJobId, retrieveJob, } from "@perstack/filesystem-storage" diff --git a/apps/perstack/src/lib/log/filter.test.ts b/packages/log/src/filter.test.ts similarity index 100% rename from apps/perstack/src/lib/log/filter.test.ts rename to packages/log/src/filter.test.ts diff --git a/apps/perstack/src/lib/log/filter.ts b/packages/log/src/filter.ts similarity index 100% rename from apps/perstack/src/lib/log/filter.ts rename to packages/log/src/filter.ts diff --git a/apps/perstack/src/lib/log/formatter.test.ts b/packages/log/src/formatter.test.ts similarity index 100% rename from apps/perstack/src/lib/log/formatter.test.ts rename to packages/log/src/formatter.test.ts diff --git a/apps/perstack/src/lib/log/formatter.ts b/packages/log/src/formatter.ts similarity index 98% rename from apps/perstack/src/lib/log/formatter.ts rename to packages/log/src/formatter.ts index db5307c7..26338546 100644 --- a/apps/perstack/src/lib/log/formatter.ts +++ b/packages/log/src/formatter.ts @@ -80,11 +80,11 @@ export function formatTerminal(output: LogOutput, options: FormatterOptions): st } if (output.events.length > 0) { lines.push("Events:") - lines.push("─".repeat(50)) + lines.push("\u2500".repeat(50)) for (const event of output.events) { lines.push(...formatEvent(event, options.verbose)) } - lines.push("─".repeat(50)) + lines.push("\u2500".repeat(50)) // Show pagination info if events were truncated // Use matchedAfterPagination to compare with totalBeforeLimit, not events.length // (events.length may include context events which could exceed matchedAfterPagination) @@ -188,7 +188,7 @@ function formatEvent(event: RunEvent, verbose: boolean): string[] { for (const tr of resultEvent.toolResults) { const text = extractTextContent(tr.result) const isError = text.toLowerCase().startsWith("error") - const symbol = isError ? "✗" : "✓" + const symbol = isError ? "\u2717" : "\u2713" const preview = text.length > 40 ? `${text.slice(0, 40)}...` : text lines.push(` ${symbol} ${tr.toolName}: ${preview}`) } diff --git a/apps/perstack/src/log.ts b/packages/log/src/handler.ts similarity index 69% rename from apps/perstack/src/log.ts rename to packages/log/src/handler.ts index 72c3a513..b0b536c8 100644 --- a/apps/perstack/src/log.ts +++ b/packages/log/src/handler.ts @@ -1,4 +1,3 @@ -import { Command } from "commander" import { applyFilters, createLogDataFetcher, @@ -12,12 +11,12 @@ import { type LogOutput, parseFilterExpression, parseStepFilter, -} from "./lib/log/index.js" +} from "./index.js" const DEFAULT_TAKE = 100 const DEFAULT_OFFSET = 0 -function parsePositiveInt(val: string, optionName: string): number { +export function parsePositiveInt(val: string, optionName: string): number { const parsed = parseInt(val, 10) if (Number.isNaN(parsed)) { throw new Error(`Invalid value for ${optionName}: "${val}" is not a valid number`) @@ -28,59 +27,31 @@ function parsePositiveInt(val: string, optionName: string): number { return parsed } -export const logCommand = new Command() - .command("log") - .description("View execution history and events for debugging") - .option("--job ", "Show events for a specific job") - .option("--run ", "Show events for a specific run") - .option("--checkpoint ", "Show checkpoint details") - .option("--step ", "Filter by step number (e.g., 5, >5, 1-10)") - .option("--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 ", "Simple filter expression") - .option("--json", "Output as JSON") - .option("--pretty", "Pretty-print JSON output") - .option("--verbose", "Show full event details") - .option( - "--take ", - `Number of events to display (default: ${DEFAULT_TAKE}, use 0 for all)`, - (val) => parsePositiveInt(val, "--take"), - ) - .option("--offset ", `Number of events to skip (default: ${DEFAULT_OFFSET})`, (val) => - parsePositiveInt(val, "--offset"), - ) - .option("--context ", "Include N events before/after matches", (val) => - parsePositiveInt(val, "--context"), - ) - .option("--messages", "Show message history for checkpoint") - .option("--summary", "Show summarized view") - .action(async (options: LogCommandOptions) => { - try { - const storagePath = process.env.PERSTACK_STORAGE_PATH ?? `${process.cwd()}/perstack` - const adapter = createStorageAdapter(storagePath) - const fetcher = createLogDataFetcher(adapter) - const filterOptions = buildFilterOptions(options) - const formatterOptions = buildFormatterOptions(options) - const output = await buildOutput(fetcher, options, filterOptions, storagePath) - if (!output) { - console.log("No data found") - return - } - const formatted = formatterOptions.json - ? formatJson(output, formatterOptions) - : formatTerminal(output, formatterOptions) - console.log(formatted) - } catch (error) { - if (error instanceof Error) { - console.error(`Error: ${error.message}`) - } else { - console.error("An unexpected error occurred") - } - process.exit(1) +export async function logHandler(options: LogCommandOptions): Promise { + try { + const storagePath = process.env.PERSTACK_STORAGE_PATH ?? `${process.cwd()}/perstack` + const adapter = createStorageAdapter(storagePath) + const fetcher = createLogDataFetcher(adapter) + const filterOptions = buildFilterOptions(options) + const formatterOptions = buildFormatterOptions(options) + const output = await buildOutput(fetcher, options, filterOptions, storagePath) + if (!output) { + console.log("No data found") + return + } + const formatted = formatterOptions.json + ? formatJson(output, formatterOptions) + : formatTerminal(output, formatterOptions) + console.log(formatted) + } catch (error) { + if (error instanceof Error) { + console.error(`Error: ${error.message}`) + } else { + console.error("An unexpected error occurred") } - }) + process.exit(1) + } +} function buildFilterOptions(options: LogCommandOptions): FilterOptions { const filterOptions: FilterOptions = {} diff --git a/apps/perstack/src/lib/log/index.ts b/packages/log/src/index.ts similarity index 89% rename from apps/perstack/src/lib/log/index.ts rename to packages/log/src/index.ts index f7543579..af1d5280 100644 --- a/apps/perstack/src/lib/log/index.ts +++ b/packages/log/src/index.ts @@ -12,6 +12,7 @@ export { parseStepFilter, } from "./filter.js" export { createSummary, formatJson, formatTerminal } from "./formatter.js" +export { logHandler, parsePositiveInt } from "./handler.js" export type { FilterCondition, FilterOptions, diff --git a/apps/perstack/src/lib/log/types.ts b/packages/log/src/types.ts similarity index 100% rename from apps/perstack/src/lib/log/types.ts rename to packages/log/src/types.ts diff --git a/packages/log/tsconfig.json b/packages/log/tsconfig.json new file mode 100644 index 00000000..a10de72d --- /dev/null +++ b/packages/log/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tui/package.json b/packages/tui/package.json index 0de5df93..4329ed83 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -8,7 +8,8 @@ "./context": "./src/lib/context.ts", "./interactive": "./src/lib/interactive.ts", "./get-env": "./src/lib/get-env.ts", - "./perstack-toml": "./src/lib/perstack-toml.ts" + "./perstack-toml": "./src/lib/perstack-toml.ts", + "./provider-config": "./src/lib/provider-config.ts" }, "scripts": { "clean": "rm -rf dist", diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index e1efa42e..525c690c 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -1 +1,2 @@ +export { runHandler } from "./run-handler.js" export { type StartHandlerOptions, startHandler } from "./start-handler.js" diff --git a/packages/tui/src/lib/perstack-toml.ts b/packages/tui/src/lib/perstack-toml.ts index 0278bb30..633cbfdf 100644 --- a/packages/tui/src/lib/perstack-toml.ts +++ b/packages/tui/src/lib/perstack-toml.ts @@ -74,3 +74,23 @@ async function parsePerstackConfig(config: string): Promise { const toml = TOML.parse(config ?? "") return parseWithFriendlyError(perstackConfigSchema, toml, "perstack.toml") } + +export async function findConfigPath(configPath?: string): Promise { + if (configPath) { + return path.resolve(process.cwd(), configPath) + } + return await findConfigPathRecursively(process.cwd()) +} + +async function findConfigPathRecursively(cwd: string): Promise { + const configPath = path.resolve(cwd, "perstack.toml") + try { + await readFile(configPath) + return configPath + } catch { + if (cwd === path.parse(cwd).root) { + throw new Error("perstack.toml not found. Create one or specify --config path.") + } + return await findConfigPathRecursively(path.dirname(cwd)) + } +} diff --git a/packages/tui/src/lib/provider-config.ts b/packages/tui/src/lib/provider-config.ts index b5e06937..a9edd541 100644 --- a/packages/tui/src/lib/provider-config.ts +++ b/packages/tui/src/lib/provider-config.ts @@ -1,5 +1,16 @@ import type { ProviderConfig, ProviderName, ProviderTable } from "@perstack/core" +export const PROVIDER_ENV_MAP: Record = { + 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, +} + type SettingRecord = Record export function getProviderConfig( diff --git a/packages/tui/src/run-handler.ts b/packages/tui/src/run-handler.ts new file mode 100644 index 00000000..2ee58375 --- /dev/null +++ b/packages/tui/src/run-handler.ts @@ -0,0 +1,118 @@ +import { createId } from "@paralleldrive/cuid2" +import type { RunEvent, RuntimeEvent } from "@perstack/core" +import { + createFilteredEventListener, + parseWithFriendlyError, + runCommandInputSchema, + validateEventFilter, +} from "@perstack/core" +import { + createInitialJob, + defaultRetrieveCheckpoint, + defaultStoreCheckpoint, + defaultStoreEvent, + retrieveJob, + storeJob, +} from "@perstack/filesystem-storage" +import { findLockfile, loadLockfile, run as perstackRun } from "@perstack/runtime" +import { resolveRunContext } from "./lib/context.js" +import { + parseInteractiveToolCallResult, + parseInteractiveToolCallResultJson, +} from "./lib/interactive.js" + +const defaultEventListener = (event: RunEvent | RuntimeEvent) => console.log(JSON.stringify(event)) + +export async function runHandler( + expertKey: string, + query: string, + options: Record, +): Promise { + const input = parseWithFriendlyError(runCommandInputSchema, { expertKey, query, options }) + + // Validate and apply event filter if specified + let eventListener = defaultEventListener + if (input.options.filter && input.options.filter.length > 0) { + try { + const validatedTypes = validateEventFilter(input.options.filter) + const allowedTypes = new Set(validatedTypes) + eventListener = createFilteredEventListener(defaultEventListener, allowedTypes) + } catch (error) { + if (error instanceof Error) { + console.error(error.message) + } else { + console.error(error) + } + process.exit(1) + } + } + + try { + const { perstackConfig, checkpoint, env, providerConfig, model, experts } = + await resolveRunContext({ + configPath: input.options.config, + provider: input.options.provider, + model: input.options.model, + envPath: input.options.envPath, + continue: input.options.continue, + continueJob: input.options.continueJob, + resumeFrom: input.options.resumeFrom, + expertKey: input.expertKey, + }) + + // Load lockfile if present + const lockfilePath = findLockfile() + const lockfile = lockfilePath ? (loadLockfile(lockfilePath) ?? undefined) : undefined + + // Generate job and run IDs + const jobId = checkpoint?.jobId ?? input.options.jobId ?? createId() + const runId = createId() + + await perstackRun( + { + setting: { + jobId, + runId, + expertKey: input.expertKey, + input: input.options.interactiveToolCallResult + ? (parseInteractiveToolCallResultJson(input.query) ?? + (checkpoint + ? parseInteractiveToolCallResult(input.query, checkpoint) + : { text: input.query })) + : { text: input.query }, + experts, + model, + providerConfig, + reasoningBudget: input.options.reasoningBudget ?? perstackConfig.reasoningBudget, + maxSteps: input.options.maxSteps ?? perstackConfig.maxSteps, + maxRetries: input.options.maxRetries ?? perstackConfig.maxRetries, + timeout: input.options.timeout ?? perstackConfig.timeout, + perstackApiBaseUrl: perstackConfig.perstackApiBaseUrl, + perstackApiKey: env.PERSTACK_API_KEY, + perstackBaseSkillCommand: perstackConfig.perstackBaseSkillCommand, + env, + proxyUrl: process.env.PERSTACK_PROXY_URL, + verbose: input.options.verbose, + }, + checkpoint, + }, + { + eventListener, + storeCheckpoint: defaultStoreCheckpoint, + storeEvent: defaultStoreEvent, + retrieveCheckpoint: defaultRetrieveCheckpoint, + storeJob, + retrieveJob, + createJob: createInitialJob, + lockfile, + }, + ) + } catch (error) { + if (error instanceof Error) { + console.error(error.message) + } else { + console.error(error) + } + process.exit(1) + } +} diff --git a/packages/tui/tsup.config.ts b/packages/tui/tsup.config.ts index b82dadf5..5e2b7803 100644 --- a/packages/tui/tsup.config.ts +++ b/packages/tui/tsup.config.ts @@ -9,5 +9,6 @@ export default defineConfig({ "src/lib/interactive": "src/lib/interactive.ts", "src/lib/get-env": "src/lib/get-env.ts", "src/lib/perstack-toml": "src/lib/perstack-toml.ts", + "src/lib/provider-config": "src/lib/provider-config.ts", }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c3ea4b..b3ef8157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@perstack/tui': + specifier: workspace:* + version: link:../../packages/tui '@tsconfig/node22': specifier: ^22.0.5 version: 22.0.5 @@ -167,18 +170,6 @@ importers: apps/perstack: dependencies: - '@paralleldrive/cuid2': - specifier: ^3.3.0 - version: 3.3.0 - '@perstack/api-client': - specifier: ^0.0.55 - version: 0.0.55(@perstack/core@packages+core)(zod@4.3.6) - '@perstack/core': - specifier: workspace:* - version: link:../../packages/core - '@perstack/runtime': - specifier: workspace:* - version: link:../runtime commander: specifier: ^14.0.3 version: 14.0.3 @@ -191,13 +182,13 @@ importers: react: specifier: ^19.2.4 version: 19.2.4 - smol-toml: - specifier: ^1.6.0 - version: 1.6.0 devDependencies: - '@perstack/filesystem-storage': + '@perstack/installer': specifier: workspace:* - version: link:../../packages/filesystem + version: link:../../packages/installer + '@perstack/log': + specifier: workspace:* + version: link:../../packages/log '@perstack/tui': specifier: workspace:* version: link:../../packages/tui @@ -376,6 +367,65 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(msw@2.12.4(@types/node@25.2.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + packages/installer: + dependencies: + '@perstack/api-client': + specifier: ^0.0.55 + version: 0.0.55(@perstack/core@packages+core)(zod@4.3.6) + '@perstack/core': + specifier: workspace:* + version: link:../core + '@perstack/runtime': + specifier: workspace:* + version: link:../../apps/runtime + smol-toml: + specifier: ^1.6.0 + version: 1.6.0 + devDependencies: + '@perstack/tui': + specifier: workspace:* + version: link:../tui + '@tsconfig/node22': + specifier: ^22.0.5 + version: 22.0.5 + '@types/node': + specifier: ^25.2.3 + version: 25.2.3 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(msw@2.12.4(@types/node@25.2.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + + packages/log: + dependencies: + '@perstack/core': + specifier: workspace:* + version: link:../core + '@perstack/filesystem-storage': + specifier: workspace:* + version: link:../filesystem + devDependencies: + '@tsconfig/node22': + specifier: ^22.0.5 + version: 22.0.5 + '@types/node': + specifier: ^25.2.3 + version: 25.2.3 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(msw@2.12.4(@types/node@25.2.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + packages/providers/anthropic: dependencies: '@ai-sdk/anthropic': From e5956bc14389e647121ec67dea08e336a571c670 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Sun, 15 Feb 2026 03:41:19 +0000 Subject: [PATCH 2/2] chore: add changeset for CLI refactoring Co-Authored-By: Claude Opus 4.6 --- .changeset/thin-cli-package-extraction.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/thin-cli-package-extraction.md diff --git a/.changeset/thin-cli-package-extraction.md b/.changeset/thin-cli-package-extraction.md new file mode 100644 index 00000000..ff1ebe61 --- /dev/null +++ b/.changeset/thin-cli-package-extraction.md @@ -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