diff --git a/CHANGELOG.md b/CHANGELOG.md index 218d1d9..c5e053b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [v0.0.2] - 2026-02-11 + +- Fix PostHog telemetry +- Add Pipelex install skills when installing methods + ## [v0.0.1] - 2026-02-11 - Initial commit! diff --git a/package-lock.json b/package-lock.json index 46635f9..ec5b4c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mthds", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mthds", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "dependencies": { "@clack/prompts": "^1.0.0", diff --git a/package.json b/package.json index 6962939..f86486f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mthds", - "version": "0.0.1", + "version": "0.0.2", "description": "CLI for composable methods for AI agents. Turn your knowledge processes into executable methods.", "license": "MIT", "type": "module", diff --git a/src/cli.ts b/src/cli.ts index f8f114b..f5dbfa7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,8 +4,9 @@ import { createRequire } from "node:module"; import * as p from "@clack/prompts"; import { showBanner } from "./commands/index.js"; import { printLogo } from "./commands/index.js"; -import { installSoftware } from "./commands/setup.js"; +import { installRunner } from "./commands/setup.js"; import { installMethod } from "./commands/install.js"; +import { configSet, configGet, configList } from "./commands/config.js"; const require = createRequire(import.meta.url); const pkg = require("../package.json") as { version: string }; @@ -32,15 +33,45 @@ program await installMethod(slug); }); -// mthds setup software +// mthds setup runner const setup = program.command("setup").exitOverride(); setup - .command("software ") - .description("Install a software runtime (e.g. pipelex)") + .command("runner ") + .description("Install a runner (e.g. pipelex)") .exitOverride() .action(async (name: string) => { - await installSoftware(name); + await installRunner(name); + }); + +// mthds config set|get|list +const config = program.command("config").description("Manage configuration").exitOverride(); + +config + .command("set") + .argument("", "Config key (runner, api-url, api-key)") + .argument("", "Value to set") + .description("Set a config value") + .exitOverride() + .action(async (key: string, value: string) => { + await configSet(key, value); + }); + +config + .command("get") + .argument("", "Config key (runner, api-url, api-key)") + .description("Get a config value") + .exitOverride() + .action(async (key: string) => { + await configGet(key); + }); + +config + .command("list") + .description("List all config values") + .exitOverride() + .action(async () => { + await configList(); }); // Default: show banner diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..3855f33 --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,87 @@ +import * as p from "@clack/prompts"; +import { printLogo } from "./index.js"; +import { + VALID_KEYS, + resolveKey, + getConfigValue, + setConfigValue, + listConfig, +} from "../config/config.js"; +import type { RunnerType } from "../runners/types.js"; + +const VALID_RUNNERS: RunnerType[] = ["api", "pipelex"]; + +function isValidUrl(s: string): boolean { + try { + new URL(s); + return true; + } catch { + return false; + } +} + +export async function configSet(cliKey: string, value: string): Promise { + printLogo(); + p.intro("mthds config set"); + + const configKey = resolveKey(cliKey); + if (!configKey) { + p.log.error(`Unknown config key: ${cliKey}`); + p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`); + p.outro(""); + process.exit(1); + } + + // Validate value + if (configKey === "runner" && !VALID_RUNNERS.includes(value as RunnerType)) { + p.log.error(`Invalid runner: ${value}`); + p.log.info(`Valid runners: ${VALID_RUNNERS.join(", ")}`); + p.outro(""); + process.exit(1); + } + + if (configKey === "apiUrl" && !isValidUrl(value)) { + p.log.error(`Invalid URL: ${value}`); + p.outro(""); + process.exit(1); + } + + setConfigValue(configKey, value); + p.log.success(`${cliKey} = ${value}`); + p.outro(""); +} + +export async function configGet(cliKey: string): Promise { + printLogo(); + p.intro("mthds config get"); + + const configKey = resolveKey(cliKey); + if (!configKey) { + p.log.error(`Unknown config key: ${cliKey}`); + p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`); + p.outro(""); + process.exit(1); + } + + const { value, source } = getConfigValue(configKey); + const sourceLabel = source === "env" ? " (from env)" : source === "default" ? " (default)" : ""; + p.log.info(`${cliKey} = ${value}${sourceLabel}`); + p.outro(""); +} + +export async function configList(): Promise { + printLogo(); + p.intro("mthds config list"); + + const entries = listConfig(); + for (const entry of entries) { + const sourceLabel = + entry.source === "env" + ? " (from env)" + : entry.source === "default" + ? " (default)" + : ""; + p.log.info(`${entry.cliKey} = ${entry.value}${sourceLabel}`); + } + p.outro(""); +} diff --git a/src/commands/index.ts b/src/commands/index.ts index b1bf856..ead567b 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -41,7 +41,16 @@ export function showBanner(): void { ` ${chalk.yellow("install ")} Install a method` ); console.log( - ` ${chalk.yellow("setup software ")} Install a software runtime` + ` ${chalk.yellow("setup runner ")} Install a runner` + ); + console.log( + ` ${chalk.yellow("config set ")} Set a config value` + ); + console.log( + ` ${chalk.yellow("config get ")} Get a config value` + ); + console.log( + ` ${chalk.yellow("config list")} List all config values` ); console.log( ` ${chalk.yellow("--help")} Show this help message` @@ -52,7 +61,7 @@ export function showBanner(): void { console.log(chalk.bold(" Examples:")); console.log(` ${chalk.dim("$")} mthds install my-method-slug`); - console.log(` ${chalk.dim("$")} mthds setup software pipelex\n`); + console.log(` ${chalk.dim("$")} mthds setup runner pipelex\n`); console.log( chalk.dim(" Docs: https://pipelex.dev/docs\n") diff --git a/src/commands/install.ts b/src/commands/install.ts index 1580e9a..88760d8 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,7 +1,12 @@ import { join } from "node:path"; import { homedir } from "node:os"; import { mkdirSync } from "node:fs"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; + +const execAsync = promisify(exec); import * as p from "@clack/prompts"; +import chalk from "chalk"; import { isPipelexInstalled } from "../runtime/check.js"; import { ensureRuntime } from "../runtime/installer.js"; import { trackMethodInstall, shutdown } from "../telemetry/posthog.js"; @@ -82,18 +87,18 @@ export async function installMethod(slug: string): Promise { process.exit(0); } - // Step 3: Optional software install - const wantsSoftware = await p.confirm({ - message: "Do you want to install software now? (optional)", + // Step 3: Optional runner install + const wantsRunner = await p.confirm({ + message: "Do you want to install the runner now? (optional)", initialValue: false, }); - if (p.isCancel(wantsSoftware)) { + if (p.isCancel(wantsRunner)) { p.cancel("Installation cancelled."); process.exit(0); } - if (wantsSoftware) { + if (wantsRunner) { if (!isPipelexInstalled()) { await ensureRuntime(); p.log.success("pipelex installed."); @@ -120,6 +125,65 @@ export async function installMethod(slug: string): Promise { targetDir, }); + // Step 5: Optional Pipelex skills + const SKILLS_REPO = "https://github.com/pipelex/skills"; + const skillChoices = [ + { value: "check", label: "check", hint: "Validate and review Pipelex workflow bundles without making changes" }, + { value: "edit", label: "edit", hint: "Modify existing Pipelex workflow bundles" }, + { value: "build", label: "build", hint: "Create new Pipelex workflow bundles from scratch" }, + { value: "fix", label: "fix", hint: "Automatically fix issues in Pipelex workflow bundles" }, + ]; + + let selectedSkills: string[] = []; + let emptyAttempts = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + const hint = emptyAttempts > 0 + ? chalk.yellow(" press space to select, enter to confirm") + : chalk.dim(" press space to select, enter to confirm"); + + const result = await p.multiselect({ + message: `Which Pipelex skills do you want to install?\n${hint}`, + options: skillChoices, + required: false, + }); + + if (p.isCancel(result)) { + p.cancel("Installation cancelled."); + process.exit(0); + } + + if (result.length === 0) { + emptyAttempts++; + if (emptyAttempts >= 2) { + break; + } + continue; + } + + selectedSkills = result; + break; + } + + if (selectedSkills.length > 0) { + const globalFlag = selectedLocation === Loc.Global ? " -g" : ""; + const locationLabel = selectedLocation === Loc.Global ? "globally" : "locally"; + const sk = p.spinner(); + for (const skill of selectedSkills) { + sk.start(`Installing skill "${skill}" ${locationLabel}...`); + try { + await execAsync(`npx skills add ${SKILLS_REPO} --skill ${skill} --agent ${selectedAgent}${globalFlag} -y`, { + cwd: process.cwd(), + }); + sk.stop(`Skill "${skill}" installed ${locationLabel}.`); + } catch { + sk.stop(`Failed to install skill "${skill}".`); + p.log.warning(`Could not install skill "${skill}". You can retry manually:\n npx skills add ${SKILLS_REPO} --skill ${skill} --agent ${selectedAgent}${globalFlag}`); + } + } + } + p.outro("Done"); await shutdown(); } diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 93e859a..f9b2563 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -4,13 +4,13 @@ import { ensureRuntime } from "../runtime/installer.js"; import { shutdown } from "../telemetry/posthog.js"; import { printLogo } from "./index.js"; -export async function installSoftware(name: string): Promise { +export async function installRunner(name: string): Promise { printLogo(); p.intro("mthds setup"); if (name !== "pipelex") { - p.log.error(`Unknown software: ${name}`); - p.log.info("Available software: pipelex"); + p.log.error(`Unknown runner: ${name}`); + p.log.info("Available runners: pipelex"); p.outro("Done"); process.exit(1); } diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..aca1def --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,101 @@ +import { join } from "node:path"; +import { homedir } from "node:os"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import type { RunnerType } from "../runners/types.js"; + +export interface MthdsConfig { + runner: RunnerType; + apiUrl: string; + apiKey: string; +} + +const CONFIG_DIR = join(homedir(), ".mthds"); +const CONFIG_PATH = join(CONFIG_DIR, "config.json"); + +const DEFAULTS: MthdsConfig = { + runner: "api", + apiUrl: "https://api.pipelex.com", + apiKey: "", +}; + +/** Map from config key to env var name */ +const ENV_OVERRIDES: Record = { + runner: "MTHDS_RUNNER", + apiUrl: "MTHDS_API_URL", + apiKey: "MTHDS_API_KEY", +}; + +/** Map from CLI flag names (kebab-case) to config keys */ +const KEY_ALIASES: Record = { + runner: "runner", + "api-url": "apiUrl", + "api-key": "apiKey", +}; + +export const VALID_KEYS = Object.keys(KEY_ALIASES); + +export function resolveKey(cliKey: string): keyof MthdsConfig | undefined { + return KEY_ALIASES[cliKey]; +} + +function readConfigFile(): Partial { + if (!existsSync(CONFIG_PATH)) return {}; + try { + const raw = readFileSync(CONFIG_PATH, "utf-8"); + return JSON.parse(raw) as Partial; + } catch { + return {}; + } +} + +function writeConfigFile(config: Partial): void { + mkdirSync(CONFIG_DIR, { recursive: true }); + writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8"); +} + +export function loadConfig(): MthdsConfig { + const file = readConfigFile(); + const merged: MthdsConfig = { ...DEFAULTS, ...file }; + + // Env vars take precedence + for (const [key, envName] of Object.entries(ENV_OVERRIDES)) { + const envVal = process.env[envName]; + if (envVal !== undefined) { + merged[key as keyof MthdsConfig] = envVal as never; + } + } + + return merged; +} + +export function getConfigValue(key: keyof MthdsConfig): { value: string; source: "env" | "file" | "default" } { + const envName = ENV_OVERRIDES[key]; + const envVal = process.env[envName]; + if (envVal !== undefined) { + return { value: envVal, source: "env" }; + } + + const file = readConfigFile(); + if (key in file) { + return { value: String(file[key]), source: "file" }; + } + + return { value: String(DEFAULTS[key]), source: "default" }; +} + +export function setConfigValue(key: keyof MthdsConfig, value: string): void { + const file = readConfigFile(); + (file as Record)[key] = value; + writeConfigFile(file); +} + +export function listConfig(): Array<{ key: string; cliKey: string; value: string; source: "env" | "file" | "default" }> { + const result: Array<{ key: string; cliKey: string; value: string; source: "env" | "file" | "default" }> = []; + + for (const [cliKey, configKey] of Object.entries(KEY_ALIASES)) { + const { value, source } = getConfigValue(configKey); + result.push({ key: configKey, cliKey, value, source }); + } + + return result; +} diff --git a/src/runners/types.ts b/src/runners/types.ts new file mode 100644 index 0000000..2f82443 --- /dev/null +++ b/src/runners/types.ts @@ -0,0 +1,14 @@ +export type RunnerType = "api" | "pipelex"; + +export interface RunOptions { + input?: Record; +} + +export interface RunResult { + output: unknown; +} + +export interface Runner { + readonly type: RunnerType; + run(pipe: string, options?: RunOptions): Promise; +} diff --git a/src/telemetry/posthog.ts b/src/telemetry/posthog.ts index b4cc876..4b1fa49 100644 --- a/src/telemetry/posthog.ts +++ b/src/telemetry/posthog.ts @@ -11,8 +11,9 @@ function isDisabled(): boolean { function getClient(): PostHog | null { if (isDisabled()) return null; + console.log("PostHog API Key:", POSTHOG_API_KEY); if (!client) { - client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST }); + client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST, disableGeoip: true }); } return client; }