UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

622 lines (613 loc) 21.3 kB
// @bun import { execAsync } from "./chunk-bmbzs4s8.js"; import { checkAgent0CapabilitiesStatus, installAgent0Capabilities } from "./chunk-tt572q4r.js"; import { resolveCommandContext } from "./chunk-3ahwp6fe.js"; import { getRuntimeMajor, getRuntimeMajorRelation, getRuntimeVersionReport } from "./chunk-ty7sdgd4.js"; import { detectPackageManagers, getPreferredPackageManager } from "./chunk-nbasj5jm.js"; import { EXPECTED_RUNTIME_VERSION } from "./chunk-nxy2ya5r.js"; import { AdkError, ConfigWriter, exports_dependencies } from "./chunk-p0hjqn4r.js"; import { require_semver } from "./chunk-2a5b6azq.js"; import { Uk } from "./chunk-3xrpxgq4.js"; import { __toESM } from "./chunk-dhs2bg35.js"; // src/agent-project-patcher/types.ts var UNKNOWN_AGENT_PROJECT_VERSION = "0.0.0"; // src/agent-project-patcher/agent-project-patcher.ts var semver = __toESM(require_semver(), 1); // src/agent-project-patcher/patches/agent0-capabilities-bootstrap.ts function agent0Reason(status) { if (status.state === "missing") { return "Agent(0) capabilities manifest is missing or invalid"; } if (status.state === "stale") { return status.installedVersion ? `Agent(0) capabilities are stale (${status.installedVersion} -> ${status.currentVersion})` : "Agent(0) capabilities are stale"; } return `Agent(0) capabilities are current (${status.installedVersion})`; } async function detectAgent0CapabilitiesBootstrapPatch(ctx) { const status = (ctx.checkAgent0CapabilitiesStatus ?? checkAgent0CapabilitiesStatus)(ctx.projectPath); if (status.state !== "missing") { return { state: "skipped", reason: agent0Reason(status), details: { status } }; } return { state: "pending", reason: agent0Reason(status), details: { status } }; } async function applyAgent0CapabilitiesBootstrapPatch(ctx) { await (ctx.installAgent0Capabilities ?? installAgent0Capabilities)(ctx.projectPath); } var agent0CapabilitiesBootstrapPatch = { id: "agent0-capabilities-bootstrap", introducedIn: "2.0.0", title: "Agent(0) capabilities", description: "Create the project-local Agent(0) capability bundle when it is missing.", detect: detectAgent0CapabilitiesBootstrapPatch, apply: applyAgent0CapabilitiesBootstrapPatch }; // src/agent-project-patcher/patches/dependency-snapshots.ts import * as fs from "fs/promises"; import * as path from "path"; import ts from "typescript"; async function fileExists(filePath) { return fs.access(filePath).then(() => true).catch(() => false); } function propertyNameText(name) { if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) { return name.text; } return null; } function hasLegacyDependenciesField(content) { const source = ts.createSourceFile("agent.config.ts", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); let found = false; const visit = (node) => { if (found) { return; } if (ts.isPropertyAssignment(node) && propertyNameText(node.name) === "dependencies") { found = true; return; } if (ts.isShorthandPropertyAssignment(node) && node.name.text === "dependencies") { found = true; return; } ts.forEachChild(node, visit); }; ts.forEachChild(source, visit); return found; } async function readConfig(projectPath) { try { return await fs.readFile(path.join(projectPath, "agent.config.ts"), "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; } } async function detectDependencySnapshotPatch(ctx) { const markerPath = path.join(ctx.projectPath, ".adk", "dependencies", "migration.json"); if (await fileExists(markerPath)) { return { state: "skipped", reason: "migration marker exists", details: { markerPath } }; } const config = await readConfig(ctx.projectPath); const hasConfigDependencies = config ? hasLegacyDependenciesField(config) : false; const legacyLockFiles = (await Promise.all(["dependencies.dev.lock.json", "dependencies.prod.lock.json"].map(async (name) => ({ name, exists: await fileExists(path.join(ctx.projectPath, name)) })))).filter((item) => item.exists).map((item) => item.name); if (!hasConfigDependencies && legacyLockFiles.length === 0) { return { state: "skipped", reason: "no legacy dependency state detected", details: { markerPath, legacyLockFiles } }; } return { state: "pending", reason: hasConfigDependencies ? "agent.config.ts contains a legacy dependencies field" : "legacy dependency lock file exists", details: { markerPath, legacyLockFiles, hasConfigDependencies } }; } function createMigrationClient(opts) { return new Uk({ token: opts.credentials.token, apiUrl: opts.apiUrl, workspaceId: opts.workspaceId, headers: { "x-multiple-integrations": "true" } }); } async function getMigrationContext(ctx) { if (ctx.migrationContext) { return ctx.migrationContext; } const resolveContext = ctx.resolveContext ?? resolveCommandContext; return resolveContext({ cwd: ctx.projectPath, target: "dev", require: ["project", "credentials", "workspace"], createClient: false }); } async function applyDependencySnapshotPatch(ctx) { const context = await getMigrationContext(ctx); if (!context.workspaceId) { throw new AdkError({ code: "WORKSPACE_ID_MISSING", message: "No workspace ID found for dependency migration. Run `adk link` to link your agent first.", expected: true, suggestion: "Run `adk link` first." }); } const clientFactory = ctx.clientFactory ?? createMigrationClient; const migrateFromConfig = ctx.migrateFromConfig ?? exports_dependencies.migrateFromConfig; const client = clientFactory({ credentials: context.credentials, apiUrl: context.apiUrl, workspaceId: context.workspaceId }); const result = await migrateFromConfig({ projectPath: ctx.projectPath, client }); for (const warning of result.warnings) { ctx.logger?.warn(warning.message, { event: "agent-project-patch", patch: "dependency-snapshots" }); } } var dependencySnapshotsPatch = { id: "dependency-snapshots", introducedIn: "2.0.0", title: "Dependency snapshots", description: "Move legacy dependency state to Cloud-backed .adk dependency snapshots.", detect: detectDependencySnapshotPatch, apply: applyDependencySnapshotPatch }; // src/agent-project-patcher/patches/integration-config-format.ts function manualIntegrationConfigReason(ctx, aliases) { return `Deprecated integration config entries require manual updates: ${aliases.join(", ")}. ` + "Update those entries in agent.config.ts to string shorthand, then verify integration settings in the Control Panel: " + `http://localhost:${ctx.adkDevConsolePortStr ?? "3001"}/integrations.`; } async function detectIntegrationConfigPatch(ctx) { const configWriter = new ConfigWriter(ctx.projectPath); const { deprecatedAliases, migratableAliases } = configWriter.getIntegrationConfigMigrationState(); if (deprecatedAliases.length === 0) { return { state: "skipped", reason: "no deprecated integration object entries detected", details: { deprecatedAliases, migratableAliases } }; } if (migratableAliases.length === 0) { return { state: "blocked", reason: manualIntegrationConfigReason(ctx, deprecatedAliases), details: { deprecatedAliases, migratableAliases } }; } return { state: "pending", reason: "agent.config.ts contains deprecated integration object entries that can be migrated automatically", details: { deprecatedAliases, migratableAliases } }; } async function applyIntegrationConfigPatch(ctx) { const configWriter = new ConfigWriter(ctx.projectPath); const migratedAliases = await configWriter.migrateIntegrationsToStringFormat(); const { deprecatedAliases: remainingAliases } = configWriter.getIntegrationConfigMigrationState(); if (migratedAliases.length > 0) { ctx.logger?.info(`Migrated ${migratedAliases.length} integration config entr${migratedAliases.length === 1 ? "y" : "ies"} to string shorthand`, { event: "agent-project-patch", patch: integrationConfigFormatPatch.id, aliases: migratedAliases }); } if (remainingAliases.length > 0) { ctx.logger?.warn(`Some deprecated integration config entries still need manual updates: ${remainingAliases.join(", ")}. ` + `Open http://localhost:${ctx.adkDevConsolePortStr ?? "3001"}/integrations to verify settings.`, { event: "agent-project-patch", patch: integrationConfigFormatPatch.id, aliases: remainingAliases }); } } var integrationConfigFormatPatch = { id: "integration-config-format", introducedIn: "1.18.0", title: "Integration config format", description: "Rewrite deprecated integration object entries to string shorthand when safe.", detect: detectIntegrationConfigPatch, apply: applyIntegrationConfigPatch }; // src/agent-project-patcher/patches/runtime-package-versions.ts import * as fs2 from "fs/promises"; import * as path2 from "path"; var ADK_PACKAGE_NAMES = ["@botpress/runtime", "@botpress/adk", "@botpress/evals"]; var PACKAGE_JSON_DEPENDENCY_SECTIONS = [ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies" ]; async function readPackageJson(projectPath) { const packageJsonPath = path2.join(projectPath, "package.json"); let raw; try { raw = await fs2.readFile(packageJsonPath, "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; } const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new AdkError({ code: "INVALID_PACKAGE_JSON", message: "package.json must contain an object", expected: true, suggestion: "Ensure package.json contains a valid JSON object.", details: { packageJsonPath } }); } return parsed; } function isPackageManagerManagedSpecifier(version) { return /^(workspace|catalog|file|link|portal):/.test(version); } function findRuntimePackageChanges(packageJson, targetVersion) { const desiredVersion = `^${targetVersion}`; const changes = []; let runtimeFound = false; for (const sectionName of PACKAGE_JSON_DEPENDENCY_SECTIONS) { const section = packageJson[sectionName]; if (!section || typeof section !== "object" || Array.isArray(section)) { continue; } const dependencies = section; for (const packageName of ADK_PACKAGE_NAMES) { const currentVersion = dependencies[packageName]; if (typeof currentVersion !== "string") { continue; } if (packageName === "@botpress/runtime") { runtimeFound = true; } if (isPackageManagerManagedSpecifier(currentVersion)) { continue; } if (currentVersion !== desiredVersion) { changes.push({ packageName, section: sectionName, fromVersion: currentVersion, toVersion: desiredVersion, action: "update" }); } } } if (!runtimeFound) { changes.push({ packageName: "@botpress/runtime", section: "dependencies", toVersion: desiredVersion, action: "add" }); } return changes; } function getPackageInstallCommand(ctx) { if (ctx.packageInstallCommand !== undefined) { return ctx.packageInstallCommand; } const preferred = getPreferredPackageManager(ctx.projectPath, detectPackageManagers()); return preferred?.installCommand ?? null; } function shouldInstallRuntime(report) { return report.status === "not_installed" || report.status === "invalid" || report.status === "major_mismatch" || report.status === "outdated"; } function isInstalledRuntimeNewerThanTarget(report, targetVersion) { return getRuntimeMajorRelation(report.installedVersion, targetVersion) === "newer"; } function newerRuntimeBlockedReason(report, targetVersion) { const targetMajor = getRuntimeMajor(targetVersion); const supportedMajor = targetMajor !== null ? `${targetMajor}.x` : `v${targetVersion}`; return `Project runtime v${report.installedVersion} is newer than this CLI's supported major ${supportedMajor}; run \`adk self-upgrade\` instead of patching the project.`; } async function detectRuntimePackageVersionsPatch(ctx) { const packageJsonPath = path2.join(ctx.projectPath, "package.json"); let packageJson; try { packageJson = await readPackageJson(ctx.projectPath); } catch (error) { return { state: "blocked", reason: `Could not read package.json: ${error instanceof Error ? error.message : String(error)}`, details: { packageJsonPath } }; } if (!packageJson) { return { state: "blocked", reason: "package.json is missing", details: { packageJsonPath, packageJsonMissing: true } }; } const runtimeReport = getRuntimeVersionReport(ctx.projectPath); if (isInstalledRuntimeNewerThanTarget(runtimeReport, ctx.runtimePackageVersion)) { return { state: "blocked", reason: newerRuntimeBlockedReason(runtimeReport, ctx.runtimePackageVersion), details: { packageJsonPath, runtimeStatus: runtimeReport.status } }; } const changes = findRuntimePackageChanges(packageJson, ctx.runtimePackageVersion); const needsInstall = ctx.runPackageInstall && shouldInstallRuntime(runtimeReport); if (changes.length === 0 && !needsInstall) { return { state: "skipped", reason: "ADK package versions already match the target runtime version", details: { packageJsonPath, changes, runtimeStatus: runtimeReport.status } }; } const installCommand = getPackageInstallCommand(ctx); if (ctx.runPackageInstall && !installCommand) { return { state: "blocked", reason: "No supported package manager was detected for installing updated packages", details: { packageJsonPath, changes, installCommand, runtimeStatus: runtimeReport.status } }; } return { state: "pending", reason: changes.length > 0 ? `ADK package versions need to align to ^${ctx.runtimePackageVersion}` : "@botpress/runtime needs to be installed from the current package.json", details: { packageJsonPath, changes, installCommand, runtimeStatus: runtimeReport.status } }; } async function applyRuntimePackageVersionsPatch(ctx) { const packageJson = await readPackageJson(ctx.projectPath); if (!packageJson) { throw new AdkError({ code: "PACKAGE_JSON_MISSING", message: "package.json is missing", expected: true, suggestion: "Run this command from an agent project that contains a package.json.", details: { projectPath: ctx.projectPath } }); } const runtimeReport = getRuntimeVersionReport(ctx.projectPath); if (isInstalledRuntimeNewerThanTarget(runtimeReport, ctx.runtimePackageVersion)) { throw new AdkError({ code: "RUNTIME_NEWER_THAN_CLI", message: newerRuntimeBlockedReason(runtimeReport, ctx.runtimePackageVersion), expected: true, suggestion: "Run `adk self-upgrade` to update the CLI instead of patching the project.", details: { installedVersion: runtimeReport.installedVersion, targetVersion: ctx.runtimePackageVersion } }); } const changes = findRuntimePackageChanges(packageJson, ctx.runtimePackageVersion); const shouldInstall = ctx.runPackageInstall && (changes.length > 0 || shouldInstallRuntime(runtimeReport)); if (changes.length === 0 && !shouldInstall) { return; } if (changes.length > 0) { for (const change of changes) { let section = packageJson[change.section]; if (!section || typeof section !== "object" || Array.isArray(section)) { section = {}; packageJson[change.section] = section; } const dependencies = section; dependencies[change.packageName] = change.toVersion; } await fs2.writeFile(path2.join(ctx.projectPath, "package.json"), JSON.stringify(packageJson, null, 2) + ` `, "utf8"); } if (!shouldInstall) { return; } const installCommand = getPackageInstallCommand(ctx); if (!installCommand) { throw new AdkError({ code: "NO_PACKAGE_MANAGER", message: "No supported package manager was detected for installing updated packages", expected: true, suggestion: "Install a supported package manager (npm, pnpm, yarn, or bun) and retry." }); } if (ctx.runCommand) { await ctx.runCommand(installCommand, ctx.projectPath); } else { await execAsync(installCommand, { cwd: ctx.projectPath, env: { ...process.env } }); } } var runtimePackageVersionsPatch = { id: "runtime-package-versions", introducedIn: "0.0.0", title: "ADK package versions", description: "Align @botpress runtime packages in package.json with the target runtime version.", detect: detectRuntimePackageVersionsPatch, apply: applyRuntimePackageVersionsPatch }; // src/agent-project-patcher/patches.ts var AGENT_PROJECT_PATCHES = [ runtimePackageVersionsPatch, integrationConfigFormatPatch, dependencySnapshotsPatch, agent0CapabilitiesBootstrapPatch ]; // src/agent-project-patcher/agent-project-patcher.ts class AgentProjectPatcher { ctx; patches; constructor(options, patches = AGENT_PROJECT_PATCHES) { this.ctx = { ...options, fromVersion: options.fromVersion ?? UNKNOWN_AGENT_PROJECT_VERSION, runtimePackageVersion: options.runtimePackageVersion ?? EXPECTED_RUNTIME_VERSION, runPackageInstall: options.runPackageInstall ?? true }; this.patches = patches; } async plan() { const items = []; for (const patch of this.getEligiblePatches()) { items.push(await this.planPatch(patch)); } return this.buildPlan(items); } async apply(plan) { const sourcePlan = plan ?? await this.plan(); const items = []; for (const plannedItem of sourcePlan.items) { if (plannedItem.state === "blocked") { items.push({ ...plannedItem, status: "blocked" }); continue; } if (!plannedItem.pending) { items.push({ ...plannedItem, status: "skipped" }); continue; } const patch = this.getPatch(plannedItem.id); const item = await this.planPatch(patch); if (item.state === "blocked") { items.push({ ...item, status: "blocked" }); continue; } if (!item.pending) { items.push({ ...item, status: "skipped" }); continue; } try { await patch.apply(this.ctx, item); items.push({ ...item, status: "applied" }); } catch (error) { const message = error instanceof Error ? error.message : String(error); items.push({ ...item, status: "failed", error: message }); this.ctx.logger?.warn(`Project patch failed (${item.title}): ${message}`, { event: "agent-project-patch", patch: item.id }); } } const applied = items.filter((item) => item.status === "applied"); const failed = items.filter((item) => item.status === "failed"); const blocked = items.filter((item) => item.status === "blocked"); return { projectPath: this.ctx.projectPath, fromVersion: this.ctx.fromVersion, toVersion: this.ctx.toVersion, items, applied, failed, blocked }; } getEligiblePatches() { const to = semver.parse(this.ctx.toVersion); if (!to) { throw new AdkError({ code: "INVALID_PATCH_TARGET_VERSION", message: `Invalid patch target version: ${this.ctx.toVersion}`, details: { toVersion: this.ctx.toVersion } }); } const targetReleaseVersion = `${to.major}.${to.minor}.${to.patch}`; const patchIds = this.ctx.patchIds; return this.patches.filter((patch) => { if (patchIds && !patchIds.includes(patch.id)) { return false; } const introduced = semver.parse(patch.introducedIn); return Boolean(introduced && semver.lte(introduced, targetReleaseVersion)); }); } getPatch(id) { const patch = this.patches.find((item) => item.id === id); if (!patch) { throw new AdkError({ code: "UNKNOWN_AGENT_PROJECT_PATCH", message: `Unknown agent project patch: ${id}`, details: { id } }); } return patch; } async planPatch(patch) { const detection = await patch.detect(this.ctx); return { id: patch.id, title: patch.title, description: patch.description, introducedIn: patch.introducedIn, ...detection, pending: detection.state === "pending" }; } buildPlan(items) { const pending = items.filter((item) => item.pending); const blocked = items.filter((item) => item.state === "blocked"); return { projectPath: this.ctx.projectPath, fromVersion: this.ctx.fromVersion, toVersion: this.ctx.toVersion, items, pending, blocked, hasPending: pending.length > 0, hasBlocked: blocked.length > 0 }; } } export { UNKNOWN_AGENT_PROJECT_VERSION, AgentProjectPatcher };